diff --git a/pos_order_return_scrap/README.rst b/pos_order_return_scrap/README.rst new file mode 100644 index 0000000000..3cf03458b8 --- /dev/null +++ b/pos_order_return_scrap/README.rst @@ -0,0 +1,111 @@ +========================= +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-Alpha-red.png + :target: https://odoo-community.org/page/development-status + :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 +.. |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 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** + +.. 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 +=========== + +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 + + - Nguyen Minh Chien + +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 new file mode 100644 index 0000000000..9b4296142f --- /dev/null +++ b/pos_order_return_scrap/__init__.py @@ -0,0 +1,2 @@ +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..36799501c4 --- /dev/null +++ b/pos_order_return_scrap/__manifest__.py @@ -0,0 +1,18 @@ +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +{ + "name": "Point of Sale Order Scrap", + "version": "18.0.1.0.0", + "category": "Point Of Sale", + "author": "Trobz, Odoo Community Association (OCA)", + "license": "AGPL-3", + "website": "https://github.com/OCA/pos", + "development_status": "Alpha", + "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..fefb22220b --- /dev/null +++ b/pos_order_return_scrap/i18n/fr.po @@ -0,0 +1,37 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * pos_order_return_scrap +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 18.0\n" +"Report-Msgid-Bugs-To: \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" +"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 "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 "Lignes des commandes du point de vente" + +#. module: pos_order_return_scrap +#: 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 new file mode 100644 index 0000000000..46893a3e46 --- /dev/null +++ b/pos_order_return_scrap/i18n/pos_order_return_scrap.pot @@ -0,0 +1,37 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * pos_order_return_scrap +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 18.0\n" +"Report-Msgid-Bugs-To: \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" +"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_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 new file mode 100644 index 0000000000..4226cfce3e --- /dev/null +++ b/pos_order_return_scrap/models/__init__.py @@ -0,0 +1,2 @@ +from . import pos_order_line +from . import stock_picking 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..3e15e77d8c --- /dev/null +++ b/pos_order_return_scrap/models/pos_order_line.py @@ -0,0 +1,13 @@ +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). +from odoo import fields, models + + +class PosOrderLine(models.Model): + _inherit = "pos.order.line" + + is_scrap = fields.Boolean(string="Is Scrap?", copy=False) + + 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/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..a77fe19283 --- /dev/null +++ b/pos_order_return_scrap/readme/CONTRIBUTORS.md @@ -0,0 +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 new file mode 100644 index 0000000000..08a7551803 --- /dev/null +++ b/pos_order_return_scrap/readme/DESCRIPTION.md @@ -0,0 +1,2 @@ +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 0000000000..3a0328b516 Binary files /dev/null and b/pos_order_return_scrap/static/description/icon.png differ 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..018a33c0e4 --- /dev/null +++ b/pos_order_return_scrap/static/description/index.html @@ -0,0 +1,457 @@ + + + + + +Point of Sale Order Scrap + + + +
+

Point of Sale Order Scrap

+ + +

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

+

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

+ +
+
+

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/static/description/partial_return_scrap_form.png b/pos_order_return_scrap/static/description/partial_return_scrap_form.png new file mode 100644 index 0000000000..c65178ebb1 Binary files /dev/null and b/pos_order_return_scrap/static/description/partial_return_scrap_form.png differ 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 new file mode 100644 index 0000000000..1e5339684b --- /dev/null +++ b/pos_order_return_scrap/wizard/__init__.py @@ -0,0 +1 @@ +from . import pos_partial_return_wizard_line 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 new file mode 100644 index 0000000000..adcf15a60d --- /dev/null +++ b/pos_order_return_scrap/wizard/pos_partial_return_wizard_view.xml @@ -0,0 +1,15 @@ + + + + pos.partial.return.wizard + + + + + + + + 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