From 0ca3719847843029d728e1d9fe60ba955f048d8d Mon Sep 17 00:00:00 2001 From: Nguyen Minh Chien Date: Mon, 17 May 2021 03:33:55 +0000 Subject: [PATCH 1/4] [ADD] pos_order_return_scrap: 12.0 --- pos_order_return_scrap/__init__.py | 3 ++ pos_order_return_scrap/__manifest__.py | 16 ++++++ pos_order_return_scrap/i18n/fr.po | 38 +++++++++++++ .../i18n/pos_order_return_scrap.pot | 38 +++++++++++++ pos_order_return_scrap/models/__init__.py | 3 ++ pos_order_return_scrap/models/pos_order.py | 54 +++++++++++++++++++ .../models/pos_order_line.py | 45 ++++++++++++++++ .../readme/CONTRIBUTORS.rst | 1 + pos_order_return_scrap/readme/DESCRIPTION.rst | 3 ++ pos_order_return_scrap/wizard/__init__.py | 2 + .../wizard/pos_partial_return_wizard.py | 21 ++++++++ .../wizard/pos_partial_return_wizard_view.xml | 15 ++++++ 12 files changed, 239 insertions(+) create mode 100644 pos_order_return_scrap/__init__.py create mode 100644 pos_order_return_scrap/__manifest__.py create mode 100644 pos_order_return_scrap/i18n/fr.po create mode 100644 pos_order_return_scrap/i18n/pos_order_return_scrap.pot create mode 100644 pos_order_return_scrap/models/__init__.py create mode 100644 pos_order_return_scrap/models/pos_order.py create mode 100644 pos_order_return_scrap/models/pos_order_line.py create mode 100644 pos_order_return_scrap/readme/CONTRIBUTORS.rst create mode 100644 pos_order_return_scrap/readme/DESCRIPTION.rst create mode 100644 pos_order_return_scrap/wizard/__init__.py create mode 100644 pos_order_return_scrap/wizard/pos_partial_return_wizard.py create mode 100644 pos_order_return_scrap/wizard/pos_partial_return_wizard_view.xml diff --git a/pos_order_return_scrap/__init__.py b/pos_order_return_scrap/__init__.py new file mode 100644 index 0000000000..0217b0f920 --- /dev/null +++ b/pos_order_return_scrap/__init__.py @@ -0,0 +1,3 @@ + +from . import models +from . import wizard diff --git a/pos_order_return_scrap/__manifest__.py b/pos_order_return_scrap/__manifest__.py new file mode 100644 index 0000000000..d35247b164 --- /dev/null +++ b/pos_order_return_scrap/__manifest__.py @@ -0,0 +1,16 @@ +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +{ + "name": "Point of Sale Order Scrap", + "version": "12.0.1.0.0", + "category": "Point Of Sale", + "author": "Trobz", + "license": "AGPL-3", + "depends": [ + "pos_order_return", + ], + "data": [ + "wizard/pos_partial_return_wizard_view.xml", + ], + "installable": True, +} diff --git a/pos_order_return_scrap/i18n/fr.po b/pos_order_return_scrap/i18n/fr.po new file mode 100644 index 0000000000..77f5076898 --- /dev/null +++ b/pos_order_return_scrap/i18n/fr.po @@ -0,0 +1,38 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * pos_order_return_scrap +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 12.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2021-04-20 09:55+0000\n" +"PO-Revision-Date: 2021-04-20 09:55+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_scrap +#: model:ir.model.fields,field_description:pos_order_return_scrap.field_pos_order_line__is_scrap +#: model:ir.model.fields,field_description:pos_order_return_scrap.field_pos_partial_return_wizard_line__is_scrap +msgid "Is Scrap?" +msgstr "Mettre au rebut ?" + +#. module: pos_order_return_scrap +#: model:ir.model,name:pos_order_return_scrap.model_pos_partial_return_wizard_line +msgid "Partial Return Wizard Line" +msgstr "" + +#. module: pos_order_return_scrap +#: model:ir.model,name:pos_order_return_scrap.model_pos_order_line +msgid "Point of Sale Order Lines" +msgstr "" + +#. module: pos_order_return_scrap +#: model:ir.model,name:pos_order_return_scrap.model_pos_order +msgid "Point of Sale Orders" +msgstr "" + diff --git a/pos_order_return_scrap/i18n/pos_order_return_scrap.pot b/pos_order_return_scrap/i18n/pos_order_return_scrap.pot new file mode 100644 index 0000000000..0e70db7ada --- /dev/null +++ b/pos_order_return_scrap/i18n/pos_order_return_scrap.pot @@ -0,0 +1,38 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * pos_order_return_scrap +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 12.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2021-04-20 09:55+0000\n" +"PO-Revision-Date: 2021-04-20 09:55+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_scrap +#: model:ir.model.fields,field_description:pos_order_return_scrap.field_pos_order_line__is_scrap +#: model:ir.model.fields,field_description:pos_order_return_scrap.field_pos_partial_return_wizard_line__is_scrap +msgid "Is Scrap?" +msgstr "" + +#. module: pos_order_return_scrap +#: model:ir.model,name:pos_order_return_scrap.model_pos_partial_return_wizard_line +msgid "Partial Return Wizard Line" +msgstr "" + +#. module: pos_order_return_scrap +#: model:ir.model,name:pos_order_return_scrap.model_pos_order_line +msgid "Point of Sale Order Lines" +msgstr "" + +#. module: pos_order_return_scrap +#: model:ir.model,name:pos_order_return_scrap.model_pos_order +msgid "Point of Sale Orders" +msgstr "" + diff --git a/pos_order_return_scrap/models/__init__.py b/pos_order_return_scrap/models/__init__.py new file mode 100644 index 0000000000..15e02333f8 --- /dev/null +++ b/pos_order_return_scrap/models/__init__.py @@ -0,0 +1,3 @@ + +from . import pos_order +from . import pos_order_line diff --git a/pos_order_return_scrap/models/pos_order.py b/pos_order_return_scrap/models/pos_order.py new file mode 100644 index 0000000000..9a115fc32b --- /dev/null +++ b/pos_order_return_scrap/models/pos_order.py @@ -0,0 +1,54 @@ + +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html + +from odoo import models, api + + +class PosOrder(models.Model): + _inherit = "pos.order" + + def partial_refund(self, partial_return_wizard): + """ + Overwrite this function completely in order to add the value + for `is_scrap` + """ + 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( + { + 'order_id': new_order.id, + 'returned_line_id': wizard_line.pos_order_line_id.id, + 'qty': qty, + 'is_scrap': wizard_line.is_scrap + } + ) + copy_line._onchange_amount_line_all() + new_order._onchange_amount_all() + return res + + def _force_picking_done(self, picking): + """ + Force picking in order to be set as done. + Then check and move some scrap products + """ + super(PosOrder, self)._force_picking_done(picking) + if picking.state == 'done': + lines = self.lines.filtered(lambda l: l.is_scrap) + StockScrap = self.env['stock.scrap'].sudo() + for line in lines: + vals = StockScrap.default_get(StockScrap.fields_get_keys()) + vals.update({ + 'product_id': line.product_id.id, + 'picking_id': picking.id, + 'scrap_qty': abs(line.qty) + }) + new_record = StockScrap.new(vals) + new_record._onchange_picking_id() + new_record.onchange_product_id() + vals = StockScrap._convert_to_write(new_record._cache) + scrap = StockScrap.create(vals) + scrap.action_validate() diff --git a/pos_order_return_scrap/models/pos_order_line.py b/pos_order_return_scrap/models/pos_order_line.py new file mode 100644 index 0000000000..03f003b02a --- /dev/null +++ b/pos_order_return_scrap/models/pos_order_line.py @@ -0,0 +1,45 @@ +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). +from odoo import api, models, fields, _ +from odoo.exceptions import ValidationError + + +class PosOrderLine(models.Model): + _inherit = "pos.order.line" + + is_scrap = fields.Boolean( + string="Is Scrap?", + copy=False + ) + + @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 %.3f %s of %s because the original " + "Order line only mentions %.3f %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 %.3f %s of %s because some refunds" + " have already been done.\n Maximum quantity allowed :" + " %.3f %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 (%.3f %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_scrap/readme/CONTRIBUTORS.rst b/pos_order_return_scrap/readme/CONTRIBUTORS.rst new file mode 100644 index 0000000000..d554928aa4 --- /dev/null +++ b/pos_order_return_scrap/readme/CONTRIBUTORS.rst @@ -0,0 +1 @@ +* Trobz diff --git a/pos_order_return_scrap/readme/DESCRIPTION.rst b/pos_order_return_scrap/readme/DESCRIPTION.rst new file mode 100644 index 0000000000..c6e0bb9ec9 --- /dev/null +++ b/pos_order_return_scrap/readme/DESCRIPTION.rst @@ -0,0 +1,3 @@ +This module extends the functionality of `Point of Sale Order Return`. + +With this module, You can move some products to scrap immediately diff --git a/pos_order_return_scrap/wizard/__init__.py b/pos_order_return_scrap/wizard/__init__.py new file mode 100644 index 0000000000..ee446c7cf5 --- /dev/null +++ b/pos_order_return_scrap/wizard/__init__.py @@ -0,0 +1,2 @@ + +from . import pos_partial_return_wizard diff --git a/pos_order_return_scrap/wizard/pos_partial_return_wizard.py b/pos_order_return_scrap/wizard/pos_partial_return_wizard.py new file mode 100644 index 0000000000..82a1a11626 --- /dev/null +++ b/pos_order_return_scrap/wizard/pos_partial_return_wizard.py @@ -0,0 +1,21 @@ +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +from odoo import fields, models +from odoo.addons import decimal_precision as dp + + +class PosPartialReturnWizardLine(models.TransientModel): + _inherit = 'pos.partial.return.wizard.line' + + is_scrap = fields.Boolean( + string="Is Scrap?" + ) + initial_qty = fields.Float( + digits=dp.get_precision('Product Unit of Measure') + ) + max_returnable_qty = fields.Float( + digits=dp.get_precision('Product Unit of Measure') + ) + qty = fields.Float( + digits=dp.get_precision('Product Unit of Measure') + ) diff --git a/pos_order_return_scrap/wizard/pos_partial_return_wizard_view.xml b/pos_order_return_scrap/wizard/pos_partial_return_wizard_view.xml new file mode 100644 index 0000000000..e6a480de15 --- /dev/null +++ b/pos_order_return_scrap/wizard/pos_partial_return_wizard_view.xml @@ -0,0 +1,15 @@ + + + + pos.partial.return.wizard + + + + + + + + From f436e2fd444ad2e0ae15e2f30fabae5d7445763a Mon Sep 17 00:00:00 2001 From: Nguyen Minh Chien Date: Thu, 23 Oct 2025 18:03:45 +0700 Subject: [PATCH 2/4] [IMP] pos_order_return_scrap: pre-commit auto fixes --- pos_order_return_scrap/README.rst | 78 ++++ pos_order_return_scrap/__init__.py | 1 - pos_order_return_scrap/models/__init__.py | 1 - pos_order_return_scrap/models/pos_order.py | 29 +- .../models/pos_order_line.py | 81 ++-- pos_order_return_scrap/pyproject.toml | 3 + pos_order_return_scrap/readme/CONTRIBUTORS.md | 1 + .../readme/CONTRIBUTORS.rst | 1 - pos_order_return_scrap/readme/DESCRIPTION.md | 3 + pos_order_return_scrap/readme/DESCRIPTION.rst | 3 - .../static/description/index.html | 424 ++++++++++++++++++ pos_order_return_scrap/wizard/__init__.py | 1 - .../wizard/pos_partial_return_wizard.py | 17 +- 13 files changed, 578 insertions(+), 65 deletions(-) create mode 100644 pos_order_return_scrap/README.rst create mode 100644 pos_order_return_scrap/pyproject.toml create mode 100644 pos_order_return_scrap/readme/CONTRIBUTORS.md delete mode 100644 pos_order_return_scrap/readme/CONTRIBUTORS.rst create mode 100644 pos_order_return_scrap/readme/DESCRIPTION.md delete mode 100644 pos_order_return_scrap/readme/DESCRIPTION.rst create mode 100644 pos_order_return_scrap/static/description/index.html diff --git a/pos_order_return_scrap/README.rst b/pos_order_return_scrap/README.rst new file mode 100644 index 0000000000..c2b4defc1a --- /dev/null +++ b/pos_order_return_scrap/README.rst @@ -0,0 +1,78 @@ +========================= +Point of Sale Order Scrap +========================= + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:a222e5dfeb0ecb90562b6d4ae1467e0de789596332f897d2ba6bfd473f9fddfc + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png + :target: https://odoo-community.org/page/development-status + :alt: Beta +.. |badge2| image:: https://img.shields.io/badge/licence-AGPL--3-blue.png + :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html + :alt: License: AGPL-3 +.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fpos-lightgray.png?logo=github + :target: https://github.com/OCA/pos/tree/18.0/pos_order_return_scrap + :alt: OCA/pos +.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png + :target: https://translation.odoo-community.org/projects/pos-18-0/pos-18-0-pos_order_return_scrap + :alt: Translate me on Weblate +.. |badge5| image:: https://img.shields.io/badge/runboat-Try%20me-875A7B.png + :target: https://runboat.odoo-community.org/builds?repo=OCA/pos&target_branch=18.0 + :alt: Try me on Runboat + +|badge1| |badge2| |badge3| |badge4| |badge5| + +This module extends the functionality of Point of Sale Order Return. + +With this module, You can move some products to scrap immediately + +**Table of contents** + +.. contents:: + :local: + +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 to smash it by providing a detailed and welcomed +`feedback `_. + +Do not contact contributors directly about support or help with technical issues. + +Credits +======= + +Authors +------- + +* Trobz + +Contributors +------------ + +- Trobz + +Maintainers +----------- + +This module is maintained by the OCA. + +.. image:: https://odoo-community.org/logo.png + :alt: Odoo Community Association + :target: https://odoo-community.org + +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. + +This module is part of the `OCA/pos `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/pos_order_return_scrap/__init__.py b/pos_order_return_scrap/__init__.py index 0217b0f920..9b4296142f 100644 --- a/pos_order_return_scrap/__init__.py +++ b/pos_order_return_scrap/__init__.py @@ -1,3 +1,2 @@ - from . import models from . import wizard diff --git a/pos_order_return_scrap/models/__init__.py b/pos_order_return_scrap/models/__init__.py index 15e02333f8..64f952d518 100644 --- a/pos_order_return_scrap/models/__init__.py +++ b/pos_order_return_scrap/models/__init__.py @@ -1,3 +1,2 @@ - from . import pos_order from . import pos_order_line diff --git a/pos_order_return_scrap/models/pos_order.py b/pos_order_return_scrap/models/pos_order.py index 9a115fc32b..47c5637581 100644 --- a/pos_order_return_scrap/models/pos_order.py +++ b/pos_order_return_scrap/models/pos_order.py @@ -1,7 +1,6 @@ - # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html -from odoo import models, api +from odoo import models class PosOrder(models.Model): @@ -20,10 +19,10 @@ def partial_refund(self, partial_return_wizard): if qty != 0: copy_line = wizard_line.pos_order_line_id.copy( { - 'order_id': new_order.id, - 'returned_line_id': wizard_line.pos_order_line_id.id, - 'qty': qty, - 'is_scrap': wizard_line.is_scrap + "order_id": new_order.id, + "returned_line_id": wizard_line.pos_order_line_id.id, + "qty": qty, + "is_scrap": wizard_line.is_scrap, } ) copy_line._onchange_amount_line_all() @@ -35,17 +34,19 @@ def _force_picking_done(self, picking): Force picking in order to be set as done. Then check and move some scrap products """ - super(PosOrder, self)._force_picking_done(picking) - if picking.state == 'done': + super()._force_picking_done(picking) + if picking.state == "done": lines = self.lines.filtered(lambda l: l.is_scrap) - StockScrap = self.env['stock.scrap'].sudo() + StockScrap = self.env["stock.scrap"].sudo() for line in lines: vals = StockScrap.default_get(StockScrap.fields_get_keys()) - vals.update({ - 'product_id': line.product_id.id, - 'picking_id': picking.id, - 'scrap_qty': abs(line.qty) - }) + vals.update( + { + "product_id": line.product_id.id, + "picking_id": picking.id, + "scrap_qty": abs(line.qty), + } + ) new_record = StockScrap.new(vals) new_record._onchange_picking_id() new_record.onchange_product_id() diff --git a/pos_order_return_scrap/models/pos_order_line.py b/pos_order_return_scrap/models/pos_order_line.py index 03f003b02a..61a0ede21c 100644 --- a/pos_order_return_scrap/models/pos_order_line.py +++ b/pos_order_return_scrap/models/pos_order_line.py @@ -1,45 +1,60 @@ # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). -from odoo import api, models, fields, _ +from odoo import _, api, fields, models from odoo.exceptions import ValidationError class PosOrderLine(models.Model): _inherit = "pos.order.line" - is_scrap = fields.Boolean( - string="Is Scrap?", - copy=False - ) + is_scrap = fields.Boolean(string="Is Scrap?", copy=False) - @api.constrains('returned_line_id', 'qty') + @api.constrains("returned_line_id", "qty") def _check_return_qty(self): - if self.env.context.get('do_not_check_negative_qty', False): + 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 %.3f %s of %s because the original " - "Order line only mentions %.3f %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 %.3f %s of %s because some refunds" - " have already been done.\n Maximum quantity allowed :" - " %.3f %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 (%.3f %s of %s), without using " - "return wizard." - ) % (line.qty, line.product_id.uom_id.name, - line.product_id.name)) + raise ValidationError( + _( + "You can not return %.3f %s of %s because the original " + "Order line only mentions %.3f %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 %.3f %s of %s because some refunds" + " have already been done.\n Maximum quantity allowed :" + " %.3f %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 (%.3f %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_scrap/pyproject.toml b/pos_order_return_scrap/pyproject.toml new file mode 100644 index 0000000000..4231d0cccb --- /dev/null +++ b/pos_order_return_scrap/pyproject.toml @@ -0,0 +1,3 @@ +[build-system] +requires = ["whool"] +build-backend = "whool.buildapi" diff --git a/pos_order_return_scrap/readme/CONTRIBUTORS.md b/pos_order_return_scrap/readme/CONTRIBUTORS.md new file mode 100644 index 0000000000..26f3e1e89f --- /dev/null +++ b/pos_order_return_scrap/readme/CONTRIBUTORS.md @@ -0,0 +1 @@ +- Trobz \<\> diff --git a/pos_order_return_scrap/readme/CONTRIBUTORS.rst b/pos_order_return_scrap/readme/CONTRIBUTORS.rst deleted file mode 100644 index d554928aa4..0000000000 --- a/pos_order_return_scrap/readme/CONTRIBUTORS.rst +++ /dev/null @@ -1 +0,0 @@ -* Trobz diff --git a/pos_order_return_scrap/readme/DESCRIPTION.md b/pos_order_return_scrap/readme/DESCRIPTION.md new file mode 100644 index 0000000000..385f54dccd --- /dev/null +++ b/pos_order_return_scrap/readme/DESCRIPTION.md @@ -0,0 +1,3 @@ +This module extends the functionality of Point of Sale Order Return. + +With this module, You can move some products to scrap immediately diff --git a/pos_order_return_scrap/readme/DESCRIPTION.rst b/pos_order_return_scrap/readme/DESCRIPTION.rst deleted file mode 100644 index c6e0bb9ec9..0000000000 --- a/pos_order_return_scrap/readme/DESCRIPTION.rst +++ /dev/null @@ -1,3 +0,0 @@ -This module extends the functionality of `Point of Sale Order Return`. - -With this module, You can move some products to scrap immediately diff --git a/pos_order_return_scrap/static/description/index.html b/pos_order_return_scrap/static/description/index.html new file mode 100644 index 0000000000..4bb6e053cd --- /dev/null +++ b/pos_order_return_scrap/static/description/index.html @@ -0,0 +1,424 @@ + + + + + +Point of Sale Order Scrap + + + +
+

Point of Sale Order Scrap

+ + +

Beta License: AGPL-3 OCA/pos Translate me on Weblate Try me on Runboat

+

This module extends the functionality of Point of Sale Order Return.

+

With this module, You can move some products to scrap immediately

+

Table of contents

+ +
+

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 to smash it by providing a detailed and welcomed +feedback.

+

Do not contact contributors directly about support or help with technical issues.

+
+
+

Credits

+
+

Authors

+
    +
  • Trobz
  • +
+
+ +
+

Maintainers

+

This module is maintained by the OCA.

+ +Odoo Community Association + +

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.

+

This module is part of the OCA/pos project on GitHub.

+

You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

+
+
+
+ + diff --git a/pos_order_return_scrap/wizard/__init__.py b/pos_order_return_scrap/wizard/__init__.py index ee446c7cf5..56b3d06bba 100644 --- a/pos_order_return_scrap/wizard/__init__.py +++ b/pos_order_return_scrap/wizard/__init__.py @@ -1,2 +1 @@ - from . import pos_partial_return_wizard diff --git a/pos_order_return_scrap/wizard/pos_partial_return_wizard.py b/pos_order_return_scrap/wizard/pos_partial_return_wizard.py index 82a1a11626..96ed4eda0e 100644 --- a/pos_order_return_scrap/wizard/pos_partial_return_wizard.py +++ b/pos_order_return_scrap/wizard/pos_partial_return_wizard.py @@ -1,21 +1,16 @@ # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). from odoo import fields, models + from odoo.addons import decimal_precision as dp class PosPartialReturnWizardLine(models.TransientModel): - _inherit = 'pos.partial.return.wizard.line' + _inherit = "pos.partial.return.wizard.line" - is_scrap = fields.Boolean( - string="Is Scrap?" - ) - initial_qty = fields.Float( - digits=dp.get_precision('Product Unit of Measure') - ) + is_scrap = fields.Boolean(string="Is Scrap?") + initial_qty = fields.Float(digits=dp.get_precision("Product Unit of Measure")) max_returnable_qty = fields.Float( - digits=dp.get_precision('Product Unit of Measure') - ) - qty = fields.Float( - digits=dp.get_precision('Product Unit of Measure') + digits=dp.get_precision("Product Unit of Measure") ) + qty = fields.Float(digits=dp.get_precision("Product Unit of Measure")) From 6ec0282a7a8f3ca1a58998dedb900ee978e99102 Mon Sep 17 00:00:00 2001 From: Nguyen Minh Chien Date: Fri, 24 Oct 2025 11:26:15 +0700 Subject: [PATCH 3/4] [MIG] pos_order_return_scrap: Migration to 18.0 --- pos_order_return_scrap/README.rst | 41 +++- pos_order_return_scrap/__manifest__.py | 6 +- pos_order_return_scrap/i18n/fr.po | 21 +- .../i18n/pos_order_return_scrap.pot | 17 +- pos_order_return_scrap/models/__init__.py | 2 +- pos_order_return_scrap/models/pos_order.py | 55 ----- .../models/pos_order_line.py | 57 +---- .../models/stock_picking.py | 73 +++++++ pos_order_return_scrap/readme/CONTRIBUTORS.md | 1 + pos_order_return_scrap/readme/DESCRIPTION.md | 5 +- pos_order_return_scrap/readme/USAGE.md | 12 ++ .../static/description/icon.png | Bin 0 -> 9455 bytes .../static/description/index.html | 61 ++++-- .../description/partial_return_scrap_form.png | Bin 0 -> 25150 bytes pos_order_return_scrap/tests/__init__.py | 1 + .../tests/test_pos_order_return_scrap.py | 204 ++++++++++++++++++ pos_order_return_scrap/wizard/__init__.py | 2 +- .../wizard/pos_partial_return_wizard.py | 16 -- .../wizard/pos_partial_return_wizard_line.py | 9 + .../wizard/pos_partial_return_wizard_view.xml | 2 +- 20 files changed, 416 insertions(+), 169 deletions(-) delete mode 100644 pos_order_return_scrap/models/pos_order.py create mode 100644 pos_order_return_scrap/models/stock_picking.py create mode 100644 pos_order_return_scrap/readme/USAGE.md create mode 100644 pos_order_return_scrap/static/description/icon.png create mode 100644 pos_order_return_scrap/static/description/partial_return_scrap_form.png create mode 100644 pos_order_return_scrap/tests/__init__.py create mode 100644 pos_order_return_scrap/tests/test_pos_order_return_scrap.py delete mode 100644 pos_order_return_scrap/wizard/pos_partial_return_wizard.py create mode 100644 pos_order_return_scrap/wizard/pos_partial_return_wizard_line.py diff --git a/pos_order_return_scrap/README.rst b/pos_order_return_scrap/README.rst index c2b4defc1a..3cf03458b8 100644 --- a/pos_order_return_scrap/README.rst +++ b/pos_order_return_scrap/README.rst @@ -10,9 +10,9 @@ Point of Sale Order Scrap !! source digest: sha256:a222e5dfeb0ecb90562b6d4ae1467e0de789596332f897d2ba6bfd473f9fddfc !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png +.. |badge1| image:: https://img.shields.io/badge/maturity-Alpha-red.png :target: https://odoo-community.org/page/development-status - :alt: Beta + :alt: Alpha .. |badge2| image:: https://img.shields.io/badge/licence-AGPL--3-blue.png :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html :alt: License: AGPL-3 @@ -28,15 +28,46 @@ Point of Sale Order Scrap |badge1| |badge2| |badge3| |badge4| |badge5| -This module extends the functionality of Point of Sale Order Return. +This module extends the capabilities of the OCA module pos_order_return +by adding finer control over stock for returned items. A new option "Is +Scrap?" is introduced in the POS Partial Return Wizard. When checked, +the system automatically creates a scrap order for the returned +quantities of that product, improving warehouse accuracy for damaged or +non-sellable goods. -With this module, You can move some products to scrap immediately +.. IMPORTANT:: + This is an alpha version, the data model and design can change at any time without warning. + Only for development or testing purpose, do not use in production. + `More details on development status `_ **Table of contents** .. contents:: :local: +Usage +===== + +- Go to Point of Sale and locate an existing POS Order + +- Click "Partial Return" + +- A popup will open |image1| + +- For any product being returned as damaged or unusable: + + - Enable the checkbox "Is Scrap?" + +- Confirm the return + +Once validated: + +- Standard return processing occurs +- A Scrap Order is automatically created for each scrap-marked product + line + +.. |image1| image:: https://raw.githubusercontent.com/OCA/pos/18.0/pos_order_return_scrap/static/description/partial_return_scrap_form.png + Bug Tracker =========== @@ -60,6 +91,8 @@ Contributors - Trobz + - Nguyen Minh Chien + Maintainers ----------- diff --git a/pos_order_return_scrap/__manifest__.py b/pos_order_return_scrap/__manifest__.py index d35247b164..36799501c4 100644 --- a/pos_order_return_scrap/__manifest__.py +++ b/pos_order_return_scrap/__manifest__.py @@ -2,10 +2,12 @@ { "name": "Point of Sale Order Scrap", - "version": "12.0.1.0.0", + "version": "18.0.1.0.0", "category": "Point Of Sale", - "author": "Trobz", + "author": "Trobz, Odoo Community Association (OCA)", "license": "AGPL-3", + "website": "https://github.com/OCA/pos", + "development_status": "Alpha", "depends": [ "pos_order_return", ], diff --git a/pos_order_return_scrap/i18n/fr.po b/pos_order_return_scrap/i18n/fr.po index 77f5076898..fefb22220b 100644 --- a/pos_order_return_scrap/i18n/fr.po +++ b/pos_order_return_scrap/i18n/fr.po @@ -1,14 +1,14 @@ # Translation of Odoo Server. # This file contains the translation of the following modules: -# * pos_order_return_scrap +# * pos_order_return_scrap # msgid "" msgstr "" -"Project-Id-Version: Odoo Server 12.0\n" +"Project-Id-Version: Odoo Server 18.0\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2021-04-20 09:55+0000\n" -"PO-Revision-Date: 2021-04-20 09:55+0000\n" -"Last-Translator: <>\n" +"POT-Creation-Date: 2026-01-26 03:05+0000\n" +"PO-Revision-Date: 2026-01-26 03:05+0000\n" +"Last-Translator: \n" "Language-Team: \n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -24,15 +24,14 @@ msgstr "Mettre au rebut ?" #. module: pos_order_return_scrap #: model:ir.model,name:pos_order_return_scrap.model_pos_partial_return_wizard_line msgid "Partial Return Wizard Line" -msgstr "" +msgstr "Assistant de retour partiel" #. module: pos_order_return_scrap #: model:ir.model,name:pos_order_return_scrap.model_pos_order_line msgid "Point of Sale Order Lines" -msgstr "" +msgstr "Lignes des commandes du point de vente" #. module: pos_order_return_scrap -#: model:ir.model,name:pos_order_return_scrap.model_pos_order -msgid "Point of Sale Orders" -msgstr "" - +#: model:ir.model,name:pos_order_return_scrap.model_stock_picking +msgid "Transfer" +msgstr "Transfert" diff --git a/pos_order_return_scrap/i18n/pos_order_return_scrap.pot b/pos_order_return_scrap/i18n/pos_order_return_scrap.pot index 0e70db7ada..46893a3e46 100644 --- a/pos_order_return_scrap/i18n/pos_order_return_scrap.pot +++ b/pos_order_return_scrap/i18n/pos_order_return_scrap.pot @@ -1,14 +1,14 @@ # Translation of Odoo Server. # This file contains the translation of the following modules: -# * pos_order_return_scrap +# * pos_order_return_scrap # msgid "" msgstr "" -"Project-Id-Version: Odoo Server 12.0\n" +"Project-Id-Version: Odoo Server 18.0\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2021-04-20 09:55+0000\n" -"PO-Revision-Date: 2021-04-20 09:55+0000\n" -"Last-Translator: <>\n" +"POT-Creation-Date: 2026-01-26 03:03+0000\n" +"PO-Revision-Date: 2026-01-26 03:03+0000\n" +"Last-Translator: \n" "Language-Team: \n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -32,7 +32,6 @@ msgid "Point of Sale Order Lines" msgstr "" #. module: pos_order_return_scrap -#: model:ir.model,name:pos_order_return_scrap.model_pos_order -msgid "Point of Sale Orders" -msgstr "" - +#: model:ir.model,name:pos_order_return_scrap.model_stock_picking +msgid "Transfer" +msgstr "" \ No newline at end of file diff --git a/pos_order_return_scrap/models/__init__.py b/pos_order_return_scrap/models/__init__.py index 64f952d518..4226cfce3e 100644 --- a/pos_order_return_scrap/models/__init__.py +++ b/pos_order_return_scrap/models/__init__.py @@ -1,2 +1,2 @@ -from . import pos_order from . import pos_order_line +from . import stock_picking diff --git a/pos_order_return_scrap/models/pos_order.py b/pos_order_return_scrap/models/pos_order.py deleted file mode 100644 index 47c5637581..0000000000 --- a/pos_order_return_scrap/models/pos_order.py +++ /dev/null @@ -1,55 +0,0 @@ -# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html - -from odoo import models - - -class PosOrder(models.Model): - _inherit = "pos.order" - - def partial_refund(self, partial_return_wizard): - """ - Overwrite this function completely in order to add the value - for `is_scrap` - """ - 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( - { - "order_id": new_order.id, - "returned_line_id": wizard_line.pos_order_line_id.id, - "qty": qty, - "is_scrap": wizard_line.is_scrap, - } - ) - copy_line._onchange_amount_line_all() - new_order._onchange_amount_all() - return res - - def _force_picking_done(self, picking): - """ - Force picking in order to be set as done. - Then check and move some scrap products - """ - super()._force_picking_done(picking) - if picking.state == "done": - lines = self.lines.filtered(lambda l: l.is_scrap) - StockScrap = self.env["stock.scrap"].sudo() - for line in lines: - vals = StockScrap.default_get(StockScrap.fields_get_keys()) - vals.update( - { - "product_id": line.product_id.id, - "picking_id": picking.id, - "scrap_qty": abs(line.qty), - } - ) - new_record = StockScrap.new(vals) - new_record._onchange_picking_id() - new_record.onchange_product_id() - vals = StockScrap._convert_to_write(new_record._cache) - scrap = StockScrap.create(vals) - scrap.action_validate() diff --git a/pos_order_return_scrap/models/pos_order_line.py b/pos_order_return_scrap/models/pos_order_line.py index 61a0ede21c..3e15e77d8c 100644 --- a/pos_order_return_scrap/models/pos_order_line.py +++ b/pos_order_return_scrap/models/pos_order_line.py @@ -1,6 +1,5 @@ # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). -from odoo import _, api, fields, models -from odoo.exceptions import ValidationError +from odoo import fields, models class PosOrderLine(models.Model): @@ -8,53 +7,7 @@ class PosOrderLine(models.Model): is_scrap = fields.Boolean(string="Is Scrap?", copy=False) - @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 %.3f %s of %s because the original " - "Order line only mentions %.3f %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 %.3f %s of %s because some refunds" - " have already been done.\n Maximum quantity allowed :" - " %.3f %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 (%.3f %s of %s), without using " - "return wizard." - ) - % (line.qty, line.product_id.uom_id.name, line.product_id.name) - ) + def _prepare_refund_data_partial_line(self, data, wizard_line): + data = super()._prepare_refund_data_partial_line(data, wizard_line) + data["is_scrap"] = wizard_line["is_scrap"] + return data diff --git a/pos_order_return_scrap/models/stock_picking.py b/pos_order_return_scrap/models/stock_picking.py new file mode 100644 index 0000000000..4b841356bd --- /dev/null +++ b/pos_order_return_scrap/models/stock_picking.py @@ -0,0 +1,73 @@ +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). +from odoo import api, models + + +class StockPicking(models.Model): + _inherit = "stock.picking" + + @api.model + def _create_picking_from_pos_order_lines( + self, location_dest_id, lines, picking_type, partner=False + ): + """ + Override this method to create scrap orders for lines which is_scrap = True + """ + pickings = super()._create_picking_from_pos_order_lines( + location_dest_id, lines, picking_type, partner + ) + if not pickings: + return pickings + + # Scrap the negative lines (returned lines) only + to_scrap_lines = lines.filtered( + lambda _l: _l.product_id.type == "consu" and _l.qty < 0 and _l.is_scrap + ) + if to_scrap_lines: + self._create_scraps_from_pos_order_lines(to_scrap_lines, pickings) + return pickings + + @api.model + def _create_scraps_from_pos_order_lines(self, order_lines, pickings): + """ + Create scrap orders for pos lines in both cases of stock qty updating: at real + time and at session closing + """ + scraps = self.env["stock.scrap"] + move_lines = pickings.mapped("move_line_ids").filtered( + lambda m: m.state == "done" and m.move_id._is_incoming() + ) + if not move_lines: + return scraps + # Sum return qty by product + to_scrap = {} + for order_line in order_lines: + to_scrap.setdefault(order_line.product_id, 0) + to_scrap[order_line.product_id] += abs(order_line.qty) + + StockScrap = self.env["stock.scrap"].sudo() + for product, qty in to_scrap.items(): + for move_line in move_lines: + if qty <= 0: + break + if product != move_line.product_id: + continue + scrap_qty = min(move_line.quantity_product_uom, qty) + qty -= scrap_qty + vals = StockScrap.default_get(StockScrap._fields.keys()) + vals.update( + { + "product_id": product.id, + "picking_id": move_line.picking_id.id, + "scrap_qty": scrap_qty, + } + ) + new_record = StockScrap.new(vals) + # To trigger the onchange _onchange_serial_number if possible + if move_line.lot_id: + new_record.lot_id = move_line.lot_id + vals = StockScrap._convert_to_write(new_record._cache) + scrap = StockScrap.create(vals) + # Validate the scrap order if enough stock + scrap.action_validate() + scraps |= scrap + return scraps diff --git a/pos_order_return_scrap/readme/CONTRIBUTORS.md b/pos_order_return_scrap/readme/CONTRIBUTORS.md index 26f3e1e89f..a77fe19283 100644 --- a/pos_order_return_scrap/readme/CONTRIBUTORS.md +++ b/pos_order_return_scrap/readme/CONTRIBUTORS.md @@ -1 +1,2 @@ - Trobz \<\> + - Nguyen Minh Chien \<\> \ No newline at end of file diff --git a/pos_order_return_scrap/readme/DESCRIPTION.md b/pos_order_return_scrap/readme/DESCRIPTION.md index 385f54dccd..08a7551803 100644 --- a/pos_order_return_scrap/readme/DESCRIPTION.md +++ b/pos_order_return_scrap/readme/DESCRIPTION.md @@ -1,3 +1,2 @@ -This module extends the functionality of Point of Sale Order Return. - -With this module, You can move some products to scrap immediately +This module extends the capabilities of the OCA module pos_order_return by adding finer control over stock for returned items. +A new option "Is Scrap?" is introduced in the POS Partial Return Wizard. When checked, the system automatically creates a scrap order for the returned quantities of that product, improving warehouse accuracy for damaged or non-sellable goods. diff --git a/pos_order_return_scrap/readme/USAGE.md b/pos_order_return_scrap/readme/USAGE.md new file mode 100644 index 0000000000..3da923fb0a --- /dev/null +++ b/pos_order_return_scrap/readme/USAGE.md @@ -0,0 +1,12 @@ +- Go to Point of Sale and locate an existing POS Order +- Click "Partial Return" +- A popup will open +![image1](../static/description/partial_return_scrap_form.png) + +- For any product being returned as damaged or unusable: + - Enable the checkbox "Is Scrap?" +- Confirm the return + +Once validated: +- Standard return processing occurs +- A Scrap Order is automatically created for each scrap-marked product line diff --git a/pos_order_return_scrap/static/description/icon.png b/pos_order_return_scrap/static/description/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..3a0328b516c4980e8e44cdb63fd945757ddd132d GIT binary patch literal 9455 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 z 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 Xviia!U7SGha1wx#SCgwmn*{w2TRX*I literal 0 HcmV?d00001 diff --git a/pos_order_return_scrap/static/description/index.html b/pos_order_return_scrap/static/description/index.html index 4bb6e053cd..018a33c0e4 100644 --- a/pos_order_return_scrap/static/description/index.html +++ b/pos_order_return_scrap/static/description/index.html @@ -369,23 +369,53 @@

Point of Sale Order Scrap

!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! !! source digest: sha256:a222e5dfeb0ecb90562b6d4ae1467e0de789596332f897d2ba6bfd473f9fddfc !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! --> -

Beta License: AGPL-3 OCA/pos Translate me on Weblate Try me on Runboat

-

This module extends the functionality of Point of Sale Order Return.

-

With this module, You can move some products to scrap immediately

+

Alpha License: AGPL-3 OCA/pos Translate me on Weblate Try me on Runboat

+

This module extends the capabilities of the OCA module pos_order_return +by adding finer control over stock for returned items. A new option “Is +Scrap?” is introduced in the POS Partial Return Wizard. When checked, +the system automatically creates a scrap order for the returned +quantities of that product, improving warehouse accuracy for damaged or +non-sellable goods.

+
+

Important

+

This is an alpha version, the data model and design can change at any time without warning. +Only for development or testing purpose, do not use in production. +More details on development status

+

Table of contents

+
+

Usage

+
    +
  • Go to Point of Sale and locate an existing POS Order
  • +
  • Click “Partial Return”
  • +
  • A popup will open image1
  • +
  • For any product being returned as damaged or unusable:
      +
    • Enable the checkbox “Is Scrap?”
  • +
  • Confirm the return
  • +
+

Once validated:

+
    +
  • Standard return processing occurs
  • +
  • A Scrap Order is automatically created for each scrap-marked product +line
-

Bug Tracker

+

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 to smash it by providing a detailed and welcomed @@ -393,21 +423,24 @@

Bug Tracker

Do not contact contributors directly about support or help with technical issues.

-

Credits

+

Credits

-

Authors

+

Authors

  • Trobz
-

Maintainers

+

Maintainers

This module is maintained by the OCA.

Odoo Community Association diff --git a/pos_order_return_scrap/static/description/partial_return_scrap_form.png b/pos_order_return_scrap/static/description/partial_return_scrap_form.png new file mode 100644 index 0000000000000000000000000000000000000000..c65178ebb102d628005e2b52eaf9dbe15df862a5 GIT binary patch literal 25150 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 YK#4Z{+hyz#SXK+5nm4W$so#417i>8Cs{jB1 literal 0 HcmV?d00001 diff --git a/pos_order_return_scrap/tests/__init__.py b/pos_order_return_scrap/tests/__init__.py new file mode 100644 index 0000000000..38358312b2 --- /dev/null +++ b/pos_order_return_scrap/tests/__init__.py @@ -0,0 +1 @@ +from . import test_pos_order_return_scrap diff --git a/pos_order_return_scrap/tests/test_pos_order_return_scrap.py b/pos_order_return_scrap/tests/test_pos_order_return_scrap.py new file mode 100644 index 0000000000..01232c9de1 --- /dev/null +++ b/pos_order_return_scrap/tests/test_pos_order_return_scrap.py @@ -0,0 +1,204 @@ +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). + +from odoo.addons.pos_order_return.tests.test_pos_order_return import ( + TestPOSOrderReturn, + tagged, +) + + +@tagged("post_install", "-at_install") +class TestPosOrderReturnScrap(TestPOSOrderReturn): + def setUp(self): + super().setUp() + + self.product_normal = self.env["product.product"].create( + { + "name": "Normal Product A", + "is_storable": True, + } + ) + + self.product_lot = self.env["product.product"].create( + { + "name": "Tracked by lot", + "is_storable": True, + "tracking": "lot", + } + ) + + self.product_serial = self.env["product.product"].create( + { + "name": "Tracked by serial", + "is_storable": True, + "tracking": "serial", + } + ) + + # Create lots / serials + self.lot_1 = self.env["stock.lot"].create( + { + "name": "LOT-A", + "product_id": self.product_lot.id, + } + ) + self.serial_1 = self.env["stock.lot"].create( + { + "name": "SN-001", + "product_id": self.product_serial.id, + } + ) + wh_location = self.pos_config.picking_type_id.default_location_src_id + shelf1_location = self.env["stock.location"].create( + { + "name": "shelf1", + "usage": "internal", + "location_id": wh_location.id, + } + ) + qty = 2 + # Tracked + self.env["stock.quant"]._update_available_quantity( + self.product_lot, shelf1_location, qty, lot_id=self.lot_1 + ) + # Untracked + self.env["stock.quant"]._update_available_quantity( + self.product_normal, shelf1_location, qty + ) + + 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, + "amount_tax": 0, + "amount_total": 1350, + "amount_paid": 1350, + "amount_return": 0, + "lines": [ + ( + 0, + 0, + { + "name": "POSLINE/0001", + "product_id": self.product_normal.id, + "price_unit": 225, + "price_subtotal": 450, + "price_subtotal_incl": 450, + "qty": 2.0, + }, + ), + ( + 0, + 0, + { + "name": "POSLINE/0002", + "product_id": self.product_lot.id, + "price_unit": 225, + "price_subtotal": 450, + "price_subtotal_incl": 450, + "qty": 2.0, + "pack_lot_ids": [ + [0, 0, {"lot_name": self.lot_1.name}], + ], + }, + ), + ( + 0, + 0, + { + "name": "POSLINE/0003", + "product_id": self.product_serial.id, + "price_unit": 225, + "price_subtotal": 450, + "price_subtotal_incl": 450, + "qty": 1.0, + "pack_lot_ids": [ + [0, 0, {"lot_name": self.serial_1.name}], + ], + }, + ), + ], + } + ) + 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() + res = self.pos_order.action_pos_order_invoice() + self.invoice = self.env["account.move"].browse(res["res_id"]) + + def _return_line(self, order_line, qty, is_scrap=True): + """Helper to prepare the wizard line dict""" + return ( + 0, + 0, + { + "order_line_id": order_line.id, + "qty": qty, + "is_scrap": is_scrap, + }, + ) + + def test_pos_order_partial_refund(self): + partial_refund = ( + self.env["pos.partial.return.wizard"] + .with_context( + **{ + "active_model": self.pos_order._name, + "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 + partial_refund.line_ids[0].is_scrap = True + # Return 2 items from line POSLINE/0002 + partial_refund.line_ids[1].qty = 2 + partial_refund.line_ids[1].is_scrap = True + # Return 1 item from line POSLINE/0003 + partial_refund.line_ids[2].qty = 1 + partial_refund.line_ids[2].is_scrap = True + partial_refund.confirm() + refund_order = self.pos_order.refund_order_ids + self.assertEqual(len(refund_order), 1) + self.assertEqual(len(refund_order.lines), 3) + 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() + self.assertEqual(len(refund_order.picking_ids), 1) + + # Validate: One scrap order per returned product line + scrap_orders = self.env["stock.scrap"].search( + [("picking_id", "=", refund_order.picking_ids[0].id)] + ) + self.assertEqual(len(scrap_orders), len(self.pos_order.lines)) + + # Validate each scrap line references the correct product + returned_products = self.pos_order.lines.mapped("product_id") + self.assertTrue(all(s.product_id in returned_products for s in scrap_orders)) + + # 2 scraps done, 1 draft (not enough stock) + done_scraps = scrap_orders.filtered(lambda s: s.state == "done") + self.assertEqual(len(done_scraps), 2) + self.assertEqual((scrap_orders - done_scraps).state, "draft") + + # return the correct Lot + lot = done_scraps.mapped("lot_id") + self.assertEqual(lot, self.lot_1) diff --git a/pos_order_return_scrap/wizard/__init__.py b/pos_order_return_scrap/wizard/__init__.py index 56b3d06bba..1e5339684b 100644 --- a/pos_order_return_scrap/wizard/__init__.py +++ b/pos_order_return_scrap/wizard/__init__.py @@ -1 +1 @@ -from . import pos_partial_return_wizard +from . import pos_partial_return_wizard_line diff --git a/pos_order_return_scrap/wizard/pos_partial_return_wizard.py b/pos_order_return_scrap/wizard/pos_partial_return_wizard.py deleted file mode 100644 index 96ed4eda0e..0000000000 --- a/pos_order_return_scrap/wizard/pos_partial_return_wizard.py +++ /dev/null @@ -1,16 +0,0 @@ -# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). - -from odoo import fields, models - -from odoo.addons import decimal_precision as dp - - -class PosPartialReturnWizardLine(models.TransientModel): - _inherit = "pos.partial.return.wizard.line" - - is_scrap = fields.Boolean(string="Is Scrap?") - initial_qty = fields.Float(digits=dp.get_precision("Product Unit of Measure")) - max_returnable_qty = fields.Float( - digits=dp.get_precision("Product Unit of Measure") - ) - qty = fields.Float(digits=dp.get_precision("Product Unit of Measure")) diff --git a/pos_order_return_scrap/wizard/pos_partial_return_wizard_line.py b/pos_order_return_scrap/wizard/pos_partial_return_wizard_line.py new file mode 100644 index 0000000000..3d995ab8b5 --- /dev/null +++ b/pos_order_return_scrap/wizard/pos_partial_return_wizard_line.py @@ -0,0 +1,9 @@ +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +from odoo import fields, models + + +class PosPartialReturnWizardLine(models.TransientModel): + _inherit = "pos.partial.return.wizard.line" + + is_scrap = fields.Boolean(string="Is Scrap?") diff --git a/pos_order_return_scrap/wizard/pos_partial_return_wizard_view.xml b/pos_order_return_scrap/wizard/pos_partial_return_wizard_view.xml index e6a480de15..adcf15a60d 100644 --- a/pos_order_return_scrap/wizard/pos_partial_return_wizard_view.xml +++ b/pos_order_return_scrap/wizard/pos_partial_return_wizard_view.xml @@ -7,7 +7,7 @@ ref="pos_order_return.view_partial_return_wizard_form" /> - + From 73cfba2d2349fb6e852976abbd7cbe76b9bd7b21 Mon Sep 17 00:00:00 2001 From: Nguyen Minh Chien Date: Mon, 26 Jan 2026 10:26:48 +0700 Subject: [PATCH 4/4] [DON'T MERGE] test-requirements.txt --- test-requirements.txt | 1 + 1 file changed, 1 insertion(+) create mode 100644 test-requirements.txt diff --git a/test-requirements.txt b/test-requirements.txt new file mode 100644 index 0000000000..2a87f7cae1 --- /dev/null +++ b/test-requirements.txt @@ -0,0 +1 @@ +odoo-addon-pos_order_return @ git+https://github.com/OCA/pos.git@refs/pull/1473/head#subdirectory=pos_order_return