diff --git a/CLAUDE.md b/CLAUDE.md index a966f9f..b49667d 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,7 +1,6 @@ # Kidekoop — Odoo 18.0 Custom Addons -Odoo 18.0 (OCB community) addons repo. Code in **English**, UI in **Basque/Spanish**. -Combines OCB (`ocb/`), inherited OCA addons, and our own custom addons. +Odoo 18.0 (OCB community) addons repo. Code in **English**. ## ⚠️ Critical rules (non-negotiable) @@ -80,7 +79,7 @@ Logistics & accounting: - Global patterns: `.github/copilot-instructions.md` (extended version of this file). - Guides: `docs/QWEB_BEST_PRACTICES.md`, `docs/OCA_DOCUMENTATION.md`, `docs/LAZY_LOADING.md`. - OCA `readme/` structure (DESCRIPTION, INSTALL, CONFIGURE, USAGE, CONTRIBUTORS, CREDITS). - Credits: Criptomart (author) + Elika Bilbo (funder). + Credits: Criptomart (author). ## Commits diff --git a/l10n_es_partner_ccpae/__init__.py b/l10n_es_partner_ccpae/__init__.py new file mode 100644 index 0000000..4b76c7b --- /dev/null +++ b/l10n_es_partner_ccpae/__init__.py @@ -0,0 +1,3 @@ +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). + +from . import models diff --git a/l10n_es_partner_ccpae/__manifest__.py b/l10n_es_partner_ccpae/__manifest__.py new file mode 100644 index 0000000..d31dde6 --- /dev/null +++ b/l10n_es_partner_ccpae/__manifest__.py @@ -0,0 +1,22 @@ +# Copyright 2026 Ecocentral, Criptomart +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). +{ + "name": "Partner CCPAE", + "summary": "Añade los datos del registro de operador CCPAE a los contactos.", + "version": "18.0.1.0.0", + "development_status": "Beta", + "author": "Criptomart", + "website": "https://github.com/Ecocentral/ecocentral", + "category": "Localization/Europe", + "license": "AGPL-3", + # `mail` es imprescindible: los dos campos son `tracking=True` y eso + # necesita `mail.thread` sobre `res.partner`. No hay modelos nuevos, así + # que tampoco hay `security/`. + "depends": [ + "base", + "mail", + ], + "data": [ + "views/res_partner_views.xml", + ], +} diff --git a/l10n_es_partner_ccpae/migrations/18.0.1.0.0/pre-migration.py b/l10n_es_partner_ccpae/migrations/18.0.1.0.0/pre-migration.py new file mode 100644 index 0000000..81c5b3a --- /dev/null +++ b/l10n_es_partner_ccpae/migrations/18.0.1.0.0/pre-migration.py @@ -0,0 +1,49 @@ +# Copyright 2026 Ecocentral, Criptomart +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). +"""Renombrar l'xml_id de la vista heretada del formulari de contacte. + +A la 17.0 la vista es deia `l10n_es_partner_ccpae.view_partner_form`, un nom +que xoca visualment amb `base.view_partner_form` i que no segueix la convenció +del repo (`_view_form_`). A la 18.0 passa a dir-se +`res_partner_view_form_ccpae`. + +Sense aquest script l'actualització continuaria funcionant —Odoo crearia una +vista nova i esborraria l'antiga a `_process_end`— però pel mig hi hauria dues +vistes heretades equivalents injectant la mateixa pàgina al formulari, i es +perdrien les personalitzacions que algú hagués fet sobre el registre existent. +Renombrant a `ir_model_data` es reaprofita la mateixa `ir.ui.view`. +""" + +import logging + +_logger = logging.getLogger(__name__) + +RENAMED_XMLIDS = [ + # (model, nom antic, nom nou) + ("ir.ui.view", "view_partner_form", "res_partner_view_form_ccpae"), +] + + +def migrate(cr, version): + if not version: + return + for model, old_name, new_name in RENAMED_XMLIDS: + cr.execute( + """ + UPDATE ir_model_data + SET name = %s + WHERE module = 'l10n_es_partner_ccpae' + AND name = %s + AND model = %s + AND NOT EXISTS ( + SELECT 1 FROM ir_model_data + WHERE module = 'l10n_es_partner_ccpae' + AND name = %s + ) + """, + (new_name, old_name, model, new_name), + ) + if cr.rowcount: + _logger.info( + "l10n_es_partner_ccpae: xml_id %s renombrat a %s", old_name, new_name + ) diff --git a/l10n_es_partner_ccpae/models/__init__.py b/l10n_es_partner_ccpae/models/__init__.py new file mode 100644 index 0000000..284a83f --- /dev/null +++ b/l10n_es_partner_ccpae/models/__init__.py @@ -0,0 +1,3 @@ +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). + +from . import res_partner diff --git a/l10n_es_partner_ccpae/models/res_partner.py b/l10n_es_partner_ccpae/models/res_partner.py new file mode 100644 index 0000000..cd6414e --- /dev/null +++ b/l10n_es_partner_ccpae/models/res_partner.py @@ -0,0 +1,26 @@ +# Copyright 2026 Ecocentral, Criptomart +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). + +from odoo import fields +from odoo import models + +# Organismo de control por defecto: el código CCPAE de Ecocentral. Los +# proveedores certificados por otro consejo regulador lo sobreescriben a mano. +DEFAULT_CCPAE_ORGANISMO = "ES-ECO-019-CT" + + +class ResPartner(models.Model): + _inherit = "res.partner" + + ccpae_operador = fields.Char( + string="Nº de Operador", + tracking=True, + help="Número de operador en el registro del consejo regulador de " + "producción agraria ecológica.", + ) + ccpae_organismo = fields.Char( + string="Organismo regulador", + default=DEFAULT_CCPAE_ORGANISMO, + tracking=True, + help="Código del organismo de control que certifica al operador.", + ) diff --git a/l10n_es_partner_ccpae/readme/DESCRIPTION.rst b/l10n_es_partner_ccpae/readme/DESCRIPTION.rst new file mode 100644 index 0000000..4bd79f0 --- /dev/null +++ b/l10n_es_partner_ccpae/readme/DESCRIPTION.rst @@ -0,0 +1,13 @@ +Añade a los contactos de tipo empresa los datos del registro de operador del +consejo regulador de producción agraria ecológica (CCPAE): + +* **Nº de Operador**: el número con el que el proveedor está inscrito en el + registro. +* **Organismo regulador**: el código del organismo de control que lo certifica, + por defecto ``ES-ECO-019-CT`` (CCPAE). + +Ambos campos tienen seguimiento en el chatter, de forma que cualquier cambio en +la certificación de un proveedor queda registrado. + +A pesar del nombre, el módulo **no** depende de ``l10n_es_partner`` de OCA: solo +necesita ``base`` y ``mail``. diff --git a/l10n_es_partner_ccpae/readme/USAGE.rst b/l10n_es_partner_ccpae/readme/USAGE.rst new file mode 100644 index 0000000..c17e1cf --- /dev/null +++ b/l10n_es_partner_ccpae/readme/USAGE.rst @@ -0,0 +1,8 @@ +#. Abre un contacto de tipo *Empresa* (los campos están ocultos en los + contactos de tipo persona). +#. Ve a la pestaña **CCPAE**. +#. Rellena el *Nº de Operador*. El *Organismo regulador* viene precargado con + ``ES-ECO-019-CT``; cámbialo si el proveedor está certificado por otro + consejo regulador. + +Los cambios en ambos campos quedan registrados en el chatter del contacto. diff --git a/l10n_es_partner_ccpae/tests/__init__.py b/l10n_es_partner_ccpae/tests/__init__.py new file mode 100644 index 0000000..2d9f100 --- /dev/null +++ b/l10n_es_partner_ccpae/tests/__init__.py @@ -0,0 +1,3 @@ +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). + +from . import test_res_partner_ccpae diff --git a/l10n_es_partner_ccpae/tests/test_res_partner_ccpae.py b/l10n_es_partner_ccpae/tests/test_res_partner_ccpae.py new file mode 100644 index 0000000..e792932 --- /dev/null +++ b/l10n_es_partner_ccpae/tests/test_res_partner_ccpae.py @@ -0,0 +1,67 @@ +# Copyright 2026 Ecocentral, Criptomart +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). + +from odoo.tests.common import TransactionCase +from odoo.tests.common import tagged + +from odoo.addons.l10n_es_partner_ccpae.models.res_partner import DEFAULT_CCPAE_ORGANISMO + + +@tagged("post_install", "-at_install") +class TestResPartnerCcpae(TransactionCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.partner = cls.env["res.partner"].create( + { + "name": "Productor Ecològic Test", + "is_company": True, + } + ) + # `create` crida `_track_discard()`, que apunta `None` com a valors + # inicials del registre per no traçar la pròpia creació. Mentre aquest + # `None` hi sigui, cap `write` posterior deixa rastre. Buidant el + # precommit aquí es descarta i els tests ja poden traçar. + cls._flush_tracking(cls.env) + + @staticmethod + def _flush_tracking(env): + env.flush_all() + env.cr.precommit.run() + + def _tracked_fields(self): + """Camps amb rastre al xatter del contacte, després d'un `write`. + + `mail.thread.write()` no crea el `mail.message` de seguiment al moment: + el deixa encuat com a callback de precommit (és el que desencalla + `MailCommon.flush_tracking` del core). I un cop creat cal invalidar el + contacte, perquè `message_ids` ja s'havia llegit i el one2many es queda + amb el valor antic a la memòria cau. + """ + self._flush_tracking(self.env) + self.partner.invalidate_recordset() + return self.partner.message_ids.tracking_value_ids.mapped("field_id.name") + + def test_organismo_default(self): + """Un contacte nou porta el codi del CCPAE per defecte.""" + self.assertEqual(self.partner.ccpae_organismo, DEFAULT_CCPAE_ORGANISMO) + self.assertFalse(self.partner.ccpae_operador) + + def test_campos_con_seguimiento(self): + """Escriure als camps CCPAE deixa rastre al xatter.""" + self.partner.write({"ccpae_operador": "CT-12345"}) + self.assertIn( + "ccpae_operador", + self._tracked_fields(), + "El canvi de ccpae_operador ha de quedar registrat al xatter", + ) + + self.partner.write({"ccpae_organismo": "ES-ECO-020-CT"}) + self.assertIn("ccpae_organismo", self._tracked_fields()) + + def test_vista_heredada_valida(self): + """La vista heretada s'ancora bé al formulari de `base` de la 18.0.""" + view = self.env.ref("l10n_es_partner_ccpae.res_partner_view_form_ccpae") + arch = self.env["res.partner"].get_view(view.id, "form")["arch"] + self.assertIn("ccpae_operador", arch) + self.assertIn("ccpae_organismo", arch) diff --git a/l10n_es_partner_ccpae/views/res_partner_views.xml b/l10n_es_partner_ccpae/views/res_partner_views.xml new file mode 100644 index 0000000..23dbe8a --- /dev/null +++ b/l10n_es_partner_ccpae/views/res_partner_views.xml @@ -0,0 +1,22 @@ + + + + + res.partner.form.ccpae + res.partner + + + + + + + + + + + + + diff --git a/pos_full_refund/README.rst b/pos_full_refund/README.rst new file mode 100644 index 0000000..db305a0 --- /dev/null +++ b/pos_full_refund/README.rst @@ -0,0 +1,97 @@ +=========================== +Point of Sale - Full Refund +=========================== + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:cc92aeed4d5986a6c3a0e7860d32d67c2aceec105975c3a9ffca2caab7ae128f + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |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_full_refund + :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_full_refund + :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 adds a **Do Full Refund** button to the ticket screen of the +Point of Sale. It marks every line of the selected order for refund with +its full remaining quantity — that is, the ordered quantity minus whatever +has already been refunded — and then runs the standard refund flow, so the +cashier does not have to set the quantity line by line. + +.. 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: + +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 +------- + +* Innovyou + +Contributors +------------ + +- [Innovyou] (https://www.innovyou.it): + + - Lorenzo Carta + - Lorenzo Battistini + - Valerio Paretta + +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. + +.. |maintainer-LorenzoC0| image:: https://github.com/LorenzoC0.png?size=40px + :target: https://github.com/LorenzoC0 + :alt: LorenzoC0 + +Current `maintainer `__: + +|maintainer-LorenzoC0| + +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_full_refund/__init__.py b/pos_full_refund/__init__.py new file mode 100644 index 0000000..d9d1f13 --- /dev/null +++ b/pos_full_refund/__init__.py @@ -0,0 +1 @@ +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). diff --git a/pos_full_refund/__manifest__.py b/pos_full_refund/__manifest__.py new file mode 100644 index 0000000..db35015 --- /dev/null +++ b/pos_full_refund/__manifest__.py @@ -0,0 +1,21 @@ +{ + "name": "Point of Sale - Full Refund", + "summary": "Refund every line of a ticket in one click", + "author": "Innovyou, Odoo Community Association (OCA)", + "website": "https://github.com/OCA/pos", + "development_status": "Alpha", + "category": "Point of sale", + "maintainers": ["LorenzoC0"], + "version": "18.0.1.0.0", + "license": "AGPL-3", + "depends": ["point_of_sale"], + "assets": { + "point_of_sale._assets_pos": [ + "pos_full_refund/static/src/js/pos_full_refund.esm.js", + "pos_full_refund/static/src/xml/pos_full_refund.xml", + ], + "web.assets_tests": [ + "pos_full_refund/static/tests/tours/pos_full_refund_tour.esm.js", + ], + }, +} diff --git a/pos_full_refund/readme/CONTRIBUTORS.md b/pos_full_refund/readme/CONTRIBUTORS.md new file mode 100644 index 0000000..45787d7 --- /dev/null +++ b/pos_full_refund/readme/CONTRIBUTORS.md @@ -0,0 +1,6 @@ +- [Innovyou] (https://www.innovyou.it): + - Lorenzo Carta + - Lorenzo Battistini + - Valerio Paretta +- [Criptomart](https://criptomart.net): + - Migration to 18.0 diff --git a/pos_full_refund/readme/DESCRIPTION.md b/pos_full_refund/readme/DESCRIPTION.md new file mode 100644 index 0000000..369144a --- /dev/null +++ b/pos_full_refund/readme/DESCRIPTION.md @@ -0,0 +1,5 @@ +This module adds a **Do Full Refund** button to the ticket screen of the Point +of Sale. It marks every line of the selected order for refund with its full +remaining quantity — that is, the ordered quantity minus whatever has already +been refunded — and then runs the standard refund flow, so the cashier does not +have to set the quantity line by line. diff --git a/pos_full_refund/static/description/index.html b/pos_full_refund/static/description/index.html new file mode 100644 index 0000000..3419158 --- /dev/null +++ b/pos_full_refund/static/description/index.html @@ -0,0 +1,440 @@ + + + + + +Point of Sale - Full Refund + + + +
+

Point of Sale - Full Refund

+ + +

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

+

This module adds a Do Full Refund button to the ticket screen of the +Point of Sale. It marks every line of the selected order for refund with +its full remaining quantity — that is, the ordered quantity minus whatever +has already been refunded — and then runs the standard refund flow, so the +cashier does not have to set the quantity line by line.

+
+

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

+ +
+

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

+
    +
  • Innovyou
  • +
+
+
+

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.

+

Current maintainer:

+

LorenzoC0

+

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_full_refund/static/src/js/pos_full_refund.esm.js b/pos_full_refund/static/src/js/pos_full_refund.esm.js new file mode 100644 index 0000000..a10cddb --- /dev/null +++ b/pos_full_refund/static/src/js/pos_full_refund.esm.js @@ -0,0 +1,34 @@ +import { TicketScreen } from "@point_of_sale/app/screens/ticket_screen/ticket_screen"; +import { patch } from "@web/core/utils/patch"; + +patch(TicketScreen.prototype, { + /** + * Mark every refundable line of the selected order for a full refund and + * trigger the standard refund flow. + * + * `onDoRefund` is what the "Refund" action of the ticket screen calls: it + * builds the negative lines, links the combo children, carries over the + * fiscal position and the partner, and switches to the destination order. + */ + async onDoFullRefund() { + const order = this.getSelectedOrder(); + if (!order) { + return; + } + for (const line of order.lines) { + const toRefundDetails = line + .getAllLinesInCombo() + .map((comboLine) => this.getToRefundDetail(comboLine)); + for (const toRefundDetail of toRefundDetails) { + // Per line, and net of what was already refunded: the same + // guard the core applies in `_onUpdateSelectedOrderline`. + const refundableQty = + toRefundDetail.line.qty - toRefundDetail.line.refunded_qty; + if (refundableQty > 0) { + toRefundDetail.qty = refundableQty; + } + } + } + await this.onDoRefund(); + }, +}); diff --git a/pos_full_refund/static/src/xml/pos_full_refund.xml b/pos_full_refund/static/src/xml/pos_full_refund.xml new file mode 100644 index 0000000..d69023e --- /dev/null +++ b/pos_full_refund/static/src/xml/pos_full_refund.xml @@ -0,0 +1,21 @@ + + + + + + + + + + diff --git a/pos_full_refund/static/tests/tours/pos_full_refund_tour.esm.js b/pos_full_refund/static/tests/tours/pos_full_refund_tour.esm.js new file mode 100644 index 0000000..1da9cec --- /dev/null +++ b/pos_full_refund/static/tests/tours/pos_full_refund_tour.esm.js @@ -0,0 +1,40 @@ +import * as Chrome from "@point_of_sale/../tests/tours/utils/chrome_util"; +import * as Dialog from "@point_of_sale/../tests/tours/utils/dialog_util"; +import * as Order from "@point_of_sale/../tests/tours/utils/generic_components/order_widget_util"; +import * as PaymentScreen from "@point_of_sale/../tests/tours/utils/payment_screen_util"; +import * as ProductScreen from "@point_of_sale/../tests/tours/utils/product_screen_util"; +import * as ReceiptScreen from "@point_of_sale/../tests/tours/utils/receipt_screen_util"; +import * as TicketScreen from "@point_of_sale/../tests/tours/utils/ticket_screen_util"; +import { inLeftSide } from "@point_of_sale/../tests/tours/utils/common"; +import { registry } from "@web/core/registry"; + +/** + * Sell two lines, pay, then refund the whole ticket with a single click on the + * button this module adds. The destination order must end up with every line + * negated at its full quantity, without touching the numpad. + */ +registry.category("web_tour.tours").add("pos_full_refund_tour", { + steps: () => + [ + Chrome.startPoS(), + Dialog.confirm("Open Register"), + ProductScreen.addOrderline("Desk Pad", "2", "3"), + ProductScreen.addOrderline("Letter Tray", "3", "2"), + ProductScreen.clickPayButton(), + PaymentScreen.clickPaymentMethod("Bank"), + PaymentScreen.clickValidate(), + ReceiptScreen.isShown(), + ReceiptScreen.clickNextOrder(), + + ProductScreen.clickRefund(), + TicketScreen.selectOrder("-0001"), + TicketScreen.clickControlButton("Do Full Refund"), + + { ...ProductScreen.back(), isActive: ["mobile"] }, + ProductScreen.isShown(), + inLeftSide([ + ...Order.hasLine("Desk Pad", "-2"), + ...Order.hasLine("Letter Tray", "-3"), + ]), + ].flat(), +}); diff --git a/pos_full_refund/tests/__init__.py b/pos_full_refund/tests/__init__.py new file mode 100644 index 0000000..9c1544b --- /dev/null +++ b/pos_full_refund/tests/__init__.py @@ -0,0 +1,3 @@ +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from . import test_pos_full_refund diff --git a/pos_full_refund/tests/test_pos_full_refund.py b/pos_full_refund/tests/test_pos_full_refund.py new file mode 100644 index 0000000..5cfafeb --- /dev/null +++ b/pos_full_refund/tests/test_pos_full_refund.py @@ -0,0 +1,20 @@ +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo.tests import tagged + +from odoo.addons.point_of_sale.tests.test_frontend import TestPointOfSaleHttpCommon + + +@tagged("post_install", "-at_install") +class TestPosFullRefund(TestPointOfSaleHttpCommon): + """Browser run of the "Do Full Refund" button. + + The whole module is client side, so nothing short of a tour proves the + button works: a broken handler installs cleanly and just does nothing. + Needs a browser — the dev image ships one and exposes it through + CHROME_BIN (see dev/README.md). + """ + + def test_full_refund_tour(self): + self.main_pos_config.with_user(self.pos_user).open_ui() + self.start_pos_tour("pos_full_refund_tour") diff --git a/pos_order_reorder/README.rst b/pos_order_reorder/README.rst new file mode 100644 index 0000000..31e51f6 --- /dev/null +++ b/pos_order_reorder/README.rst @@ -0,0 +1,88 @@ +====================== +Point of Sale Re-order +====================== + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:4b4023ec8132da5e584b16a1c41656c5d4b8b3074ee7762ddb79902bd30b595c + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |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-LGPL--3-blue.png + :target: http://www.gnu.org/licenses/lgpl-3.0-standalone.html + :alt: License: LGPL-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_reorder + :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_reorder + :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 allows you to re-order a paid order on a new order: +| |image| + +.. |image| image:: https://raw.githubusercontent.com/OCA/pos/18.0/pos_order_reorder/static/img/reorder_button.png + +**Table of contents** + +.. contents:: + :local: + +Configuration +============= + +Select PoS > Configuration > Settings > enable flag "Allow Reorder" + +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 +------- + +* Cetmix + +Contributors +------------ + +- Cetmix +- Dinar Gabbasov +- `Heliconia Solutions Pvt. Ltd. `__ + + - Bhavesh Heliconia + +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_reorder/__init__.py b/pos_order_reorder/__init__.py new file mode 100644 index 0000000..0650744 --- /dev/null +++ b/pos_order_reorder/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/pos_order_reorder/__manifest__.py b/pos_order_reorder/__manifest__.py new file mode 100644 index 0000000..21799f4 --- /dev/null +++ b/pos_order_reorder/__manifest__.py @@ -0,0 +1,20 @@ +{ + "name": "Point of Sale Re-order", + "version": "18.0.1.0.0", + "category": "Sales/Point of Sale", + "summary": "Simple Re-order in the Point of Sale ", + "depends": ["point_of_sale"], + "website": "https://github.com/OCA/pos", + "author": "Cetmix,Odoo Community Association (OCA)", + "images": ["static/description/banner.png"], + "data": [ + "views/res_config_settings_view.xml", + ], + "assets": { + "point_of_sale._assets_pos": [ + "pos_order_reorder/static/src/js/**/*.js", + "pos_order_reorder/static/src/xml/**/*.xml", + ], + }, + "license": "LGPL-3", +} diff --git a/pos_order_reorder/i18n/es.po b/pos_order_reorder/i18n/es.po new file mode 100644 index 0000000..c99cd62 --- /dev/null +++ b/pos_order_reorder/i18n/es.po @@ -0,0 +1,45 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * pos_order_reorder +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 16.0\n" +"Report-Msgid-Bugs-To: \n" +"PO-Revision-Date: 2023-03-07 18:23+0000\n" +"Last-Translator: Patricia Lorenzo Bartolomé \n" +"Language-Team: none\n" +"Language: es\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" +"X-Generator: Weblate 4.14.1\n" + +#. module: pos_order_reorder +#: model:ir.model.fields,field_description:pos_order_reorder.field_pos_config__allow_reorder +#: model:ir.model.fields,field_description:pos_order_reorder.field_res_config_settings__pos_allow_reorder +msgid "Allow Reorder" +msgstr "Permitir repetir un pedido" + +#. module: pos_order_reorder +#: model:ir.model,name:pos_order_reorder.model_res_config_settings +msgid "Config Settings" +msgstr "Ajustes" + +#. module: pos_order_reorder +#: model_terms:ir.ui.view,arch_db:pos_order_reorder.res_config_settings_view_form +msgid "Creating a new POS order based on existing one" +msgstr "Crear un nuevo pedido basado en uno existente" + +#. module: pos_order_reorder +#: model:ir.model,name:pos_order_reorder.model_pos_config +msgid "Point of Sale Configuration" +msgstr "Configuración Punto de Venta" + +#. module: pos_order_reorder +#. odoo-javascript +#: code:addons/pos_order_reorder/static/src/xml/Screens/TicketScreen/ControlButtons/ReorderButton.xml:0 +#, python-format +msgid "Re-order" +msgstr "Repetir pedido" diff --git a/pos_order_reorder/i18n/fr.po b/pos_order_reorder/i18n/fr.po new file mode 100644 index 0000000..8044ca3 --- /dev/null +++ b/pos_order_reorder/i18n/fr.po @@ -0,0 +1,45 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * pos_order_reorder +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 16.0\n" +"Report-Msgid-Bugs-To: \n" +"PO-Revision-Date: 2023-11-10 13:20+0000\n" +"Last-Translator: LESTRAT21 \n" +"Language-Team: none\n" +"Language: fr\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: nplurals=2; plural=n > 1;\n" +"X-Generator: Weblate 4.17\n" + +#. module: pos_order_reorder +#: model:ir.model.fields,field_description:pos_order_reorder.field_pos_config__allow_reorder +#: model:ir.model.fields,field_description:pos_order_reorder.field_res_config_settings__pos_allow_reorder +msgid "Allow Reorder" +msgstr "Autoriser la copie du ticket" + +#. module: pos_order_reorder +#: model:ir.model,name:pos_order_reorder.model_res_config_settings +msgid "Config Settings" +msgstr "Configuration" + +#. module: pos_order_reorder +#: model_terms:ir.ui.view,arch_db:pos_order_reorder.res_config_settings_view_form +msgid "Creating a new POS order based on existing one" +msgstr "Créer un nouveau ticket à partir d'un ticket existant" + +#. module: pos_order_reorder +#: model:ir.model,name:pos_order_reorder.model_pos_config +msgid "Point of Sale Configuration" +msgstr "Configuration du point de vente" + +#. module: pos_order_reorder +#. odoo-javascript +#: code:addons/pos_order_reorder/static/src/xml/Screens/TicketScreen/ControlButtons/ReorderButton.xml:0 +#, python-format +msgid "Re-order" +msgstr "Recopier le ticket" diff --git a/pos_order_reorder/i18n/it.po b/pos_order_reorder/i18n/it.po new file mode 100644 index 0000000..93d3f48 --- /dev/null +++ b/pos_order_reorder/i18n/it.po @@ -0,0 +1,45 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * pos_order_reorder +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 16.0\n" +"Report-Msgid-Bugs-To: \n" +"PO-Revision-Date: 2023-03-09 13:22+0000\n" +"Last-Translator: mymage \n" +"Language-Team: none\n" +"Language: it\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" +"X-Generator: Weblate 4.14.1\n" + +#. module: pos_order_reorder +#: model:ir.model.fields,field_description:pos_order_reorder.field_pos_config__allow_reorder +#: model:ir.model.fields,field_description:pos_order_reorder.field_res_config_settings__pos_allow_reorder +msgid "Allow Reorder" +msgstr "Consenti riordine" + +#. module: pos_order_reorder +#: model:ir.model,name:pos_order_reorder.model_res_config_settings +msgid "Config Settings" +msgstr "Impostazioni configurazione" + +#. module: pos_order_reorder +#: model_terms:ir.ui.view,arch_db:pos_order_reorder.res_config_settings_view_form +msgid "Creating a new POS order based on existing one" +msgstr "Crea un nuovo ordine POS a partire da uno esistente" + +#. module: pos_order_reorder +#: model:ir.model,name:pos_order_reorder.model_pos_config +msgid "Point of Sale Configuration" +msgstr "Configurazione punto vendita" + +#. module: pos_order_reorder +#. odoo-javascript +#: code:addons/pos_order_reorder/static/src/xml/Screens/TicketScreen/ControlButtons/ReorderButton.xml:0 +#, python-format +msgid "Re-order" +msgstr "Riordina" diff --git a/pos_order_reorder/i18n/pos_order_reorder.pot b/pos_order_reorder/i18n/pos_order_reorder.pot new file mode 100644 index 0000000..1839823 --- /dev/null +++ b/pos_order_reorder/i18n/pos_order_reorder.pot @@ -0,0 +1,42 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * pos_order_reorder +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 16.0\n" +"Report-Msgid-Bugs-To: \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_reorder +#: model:ir.model.fields,field_description:pos_order_reorder.field_pos_config__allow_reorder +#: model:ir.model.fields,field_description:pos_order_reorder.field_res_config_settings__pos_allow_reorder +msgid "Allow Reorder" +msgstr "" + +#. module: pos_order_reorder +#: model:ir.model,name:pos_order_reorder.model_res_config_settings +msgid "Config Settings" +msgstr "" + +#. module: pos_order_reorder +#: model_terms:ir.ui.view,arch_db:pos_order_reorder.res_config_settings_view_form +msgid "Creating a new POS order based on existing one" +msgstr "" + +#. module: pos_order_reorder +#: model:ir.model,name:pos_order_reorder.model_pos_config +msgid "Point of Sale Configuration" +msgstr "" + +#. module: pos_order_reorder +#. odoo-javascript +#: code:addons/pos_order_reorder/static/src/xml/Screens/TicketScreen/ControlButtons/ReorderButton.xml:0 +#, python-format +msgid "Re-order" +msgstr "" diff --git a/pos_order_reorder/models/__init__.py b/pos_order_reorder/models/__init__.py new file mode 100644 index 0000000..2b92809 --- /dev/null +++ b/pos_order_reorder/models/__init__.py @@ -0,0 +1,2 @@ +from . import pos_config +from . import res_config_settings diff --git a/pos_order_reorder/models/pos_config.py b/pos_order_reorder/models/pos_config.py new file mode 100644 index 0000000..cd11743 --- /dev/null +++ b/pos_order_reorder/models/pos_config.py @@ -0,0 +1,8 @@ +from odoo import fields +from odoo import models + + +class PosConfig(models.Model): + _inherit = "pos.config" + + allow_reorder = fields.Boolean(default=True) diff --git a/pos_order_reorder/models/res_config_settings.py b/pos_order_reorder/models/res_config_settings.py new file mode 100644 index 0000000..0a0c45f --- /dev/null +++ b/pos_order_reorder/models/res_config_settings.py @@ -0,0 +1,10 @@ +from odoo import fields +from odoo import models + + +class ResConfigSettings(models.TransientModel): + _inherit = "res.config.settings" + + pos_allow_reorder = fields.Boolean( + related="pos_config_id.allow_reorder", readonly=False + ) diff --git a/pos_order_reorder/pyproject.toml b/pos_order_reorder/pyproject.toml new file mode 100644 index 0000000..4231d0c --- /dev/null +++ b/pos_order_reorder/pyproject.toml @@ -0,0 +1,3 @@ +[build-system] +requires = ["whool"] +build-backend = "whool.buildapi" diff --git a/pos_order_reorder/readme/CONFIGURE.md b/pos_order_reorder/readme/CONFIGURE.md new file mode 100644 index 0000000..f8dbc78 --- /dev/null +++ b/pos_order_reorder/readme/CONFIGURE.md @@ -0,0 +1 @@ +Select PoS \> Configuration \> Settings \> enable flag "Allow Reorder" diff --git a/pos_order_reorder/readme/CONTRIBUTORS.md b/pos_order_reorder/readme/CONTRIBUTORS.md new file mode 100644 index 0000000..ac5ddd2 --- /dev/null +++ b/pos_order_reorder/readme/CONTRIBUTORS.md @@ -0,0 +1,4 @@ +- Cetmix \<\> +- Dinar Gabbasov +- [Heliconia Solutions Pvt. Ltd.](https://www.heliconia.io) + - Bhavesh Heliconia diff --git a/pos_order_reorder/readme/DESCRIPTION.md b/pos_order_reorder/readme/DESCRIPTION.md new file mode 100644 index 0000000..3216744 --- /dev/null +++ b/pos_order_reorder/readme/DESCRIPTION.md @@ -0,0 +1,2 @@ +This module allows you to re-order a paid order on a new order: +![image](../static/img/reorder_button.png) diff --git a/pos_order_reorder/static/description/banner.png b/pos_order_reorder/static/description/banner.png new file mode 100755 index 0000000..ace841d Binary files /dev/null and b/pos_order_reorder/static/description/banner.png differ diff --git a/pos_order_reorder/static/description/icon.png b/pos_order_reorder/static/description/icon.png new file mode 100644 index 0000000..3a0328b Binary files /dev/null and b/pos_order_reorder/static/description/icon.png differ diff --git a/pos_order_reorder/static/description/index.html b/pos_order_reorder/static/description/index.html new file mode 100644 index 0000000..d163ccf --- /dev/null +++ b/pos_order_reorder/static/description/index.html @@ -0,0 +1,436 @@ + + + + + +Point of Sale Re-order + + + +
+

Point of Sale Re-order

+ + +

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

+
+
This module allows you to re-order a paid order on a new order:
+
image
+
+

Table of contents

+ +
+

Configuration

+

Select PoS > Configuration > Settings > enable flag “Allow Reorder”

+
+
+

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

+
    +
  • Cetmix
  • +
+
+
+

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_reorder/static/img/reorder_button.png b/pos_order_reorder/static/img/reorder_button.png new file mode 100644 index 0000000..799d30e Binary files /dev/null and b/pos_order_reorder/static/img/reorder_button.png differ diff --git a/pos_order_reorder/static/src/js/Screens/TicketScreen/ControlButtons/ReorderButton.esm.js b/pos_order_reorder/static/src/js/Screens/TicketScreen/ControlButtons/ReorderButton.esm.js new file mode 100644 index 0000000..bbe050a --- /dev/null +++ b/pos_order_reorder/static/src/js/Screens/TicketScreen/ControlButtons/ReorderButton.esm.js @@ -0,0 +1,67 @@ +/** @odoo-module **/ + +import { Component } from "@odoo/owl"; +import { usePos } from "@point_of_sale/app/store/pos_hook"; + +export class ReorderButton extends Component { + static template = "ReorderButton"; + + setup() { + this.pos = usePos(); + } + get isEmptyOrder() { + if (!this.props.order) return true; + return this.props.order.is_empty(); + } + _reOrder() { + if (this.isEmptyOrder) { + return; + } + const order = this.props.order; + const pos = this.pos; + const partner = order.get_partner(); + const newOrder = pos.add_new_order(); + if (partner) { + newOrder.set_partner(partner); + } + if (order.fiscal_position) { + newOrder.fiscal_position = order.fiscal_position; + } + if (order.pricelist) { + newOrder.set_pricelist(order.pricelist); + } + const lines = order.get_orderlines(); + for (var i = 0; i < lines.length; i++) { + const line = lines[i]; + const new_line = this.props.order.models["pos.order.line"].create( + this._prepareReorderLineVals(newOrder, line) + ); + if (line.pack_lot_lines) { + new_line.setPackLotLines({ + modifiedPackLotLines: [], + newPackLotLines: (line.lot_names || []).map((name) => ({ + lot_name: name, + })), + }); + } + new_line.set_unit_price(line.get_unit_price()); + new_line.set_quantity(line.get_quantity()); + new_line.set_discount(line.get_discount()); + } + this.pos.closeScreen(); + } + _prepareReorderLineVals(order, line) { + return { + order_id: order, + product_id: line.product_id, + description: line.name, + price_unit: line.price_unit, + tax_ids: line.tax_ids.map((tax) => ["link", tax]), + price_manually_set: true, + customer_note: line.customer_note, + }; + } + _onClick() { + this._reOrder(); + } +} diff --git a/pos_order_reorder/static/src/js/Screens/TicketScreen/TicketScreen.esm.js b/pos_order_reorder/static/src/js/Screens/TicketScreen/TicketScreen.esm.js new file mode 100644 index 0000000..23259b2 --- /dev/null +++ b/pos_order_reorder/static/src/js/Screens/TicketScreen/TicketScreen.esm.js @@ -0,0 +1,7 @@ +import { TicketScreen } from "@point_of_sale/app/screens/ticket_screen/ticket_screen"; +import { patch } from "@web/core/utils/patch"; +import { ReorderButton } from "@pos_order_reorder/js/Screens/TicketScreen/ControlButtons/ReorderButton.esm"; + +patch(TicketScreen, { + components: { ...TicketScreen.components, ReorderButton }, +}); diff --git a/pos_order_reorder/static/src/xml/Screens/TicketScreen/ControlButtons/ReorderButton.xml b/pos_order_reorder/static/src/xml/Screens/TicketScreen/ControlButtons/ReorderButton.xml new file mode 100644 index 0000000..14d72d5 --- /dev/null +++ b/pos_order_reorder/static/src/xml/Screens/TicketScreen/ControlButtons/ReorderButton.xml @@ -0,0 +1,15 @@ + + + + + + + + Re-order + + + + diff --git a/pos_order_reorder/static/src/xml/Screens/TicketScreen/TicketScreen.xml b/pos_order_reorder/static/src/xml/Screens/TicketScreen/TicketScreen.xml new file mode 100644 index 0000000..f3806aa --- /dev/null +++ b/pos_order_reorder/static/src/xml/Screens/TicketScreen/TicketScreen.xml @@ -0,0 +1,14 @@ + + + + + + + + + + diff --git a/pos_order_reorder/views/res_config_settings_view.xml b/pos_order_reorder/views/res_config_settings_view.xml new file mode 100644 index 0000000..511af7e --- /dev/null +++ b/pos_order_reorder/views/res_config_settings_view.xml @@ -0,0 +1,29 @@ + + + + res.config.settings.view.form + res.config.settings + + + +
+
+ +
+
+
+
+
+
+
+
diff --git a/sort_lines_by_product_name/__init__.py b/sort_lines_by_product_name/__init__.py new file mode 100644 index 0000000..0650744 --- /dev/null +++ b/sort_lines_by_product_name/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/sort_lines_by_product_name/__manifest__.py b/sort_lines_by_product_name/__manifest__.py new file mode 100644 index 0000000..0f62fcd --- /dev/null +++ b/sort_lines_by_product_name/__manifest__.py @@ -0,0 +1,18 @@ +# Copyright 2026 Ecocentral, Criptomart +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). +{ + "name": "Sort Lines By Product Name", + "summary": "Ordena línies de venda/compra/picking/factura pel nom del producte", + "version": "18.0.1.0.0", + "development_status": "Beta", + "author": "Criptomart", + "website": "https://github.com/Ecocentral/ecocentral", + "category": "Inventory", + "license": "AGPL-3", + "depends": [ + "purchase", + "sale", + "stock", + "account_invoice_report_grouped_by_picking", + ], +} diff --git a/sort_lines_by_product_name/models/__init__.py b/sort_lines_by_product_name/models/__init__.py new file mode 100644 index 0000000..f964d5e --- /dev/null +++ b/sort_lines_by_product_name/models/__init__.py @@ -0,0 +1,5 @@ +from . import account_move +from . import product +from . import purchase_order_line +from . import sale_order_line +from . import stock_move_line diff --git a/sort_lines_by_product_name/models/account_move.py b/sort_lines_by_product_name/models/account_move.py new file mode 100644 index 0000000..c76f7b0 --- /dev/null +++ b/sort_lines_by_product_name/models/account_move.py @@ -0,0 +1,28 @@ +# Copyright 2026 Ecocentral, Criptomart +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). +from odoo import api +from odoo import fields +from odoo import models + + +class AccountMove(models.Model): + _inherit = "account.move" + + @api.model + def _sort_grouped_lines(self, lines_dic): + """Break ties on same-day pickings by name. + + ``account_invoice_report_grouped_by_picking`` (OCA) sorts groups by + ``picking.date``/``date_done`` only: when several pickings share + both (the common case for same-day deliveries), their relative + order is otherwise undefined/insertion-order, which the picking + name settles. + """ + return sorted( + lines_dic, + key=lambda x: ( + x["picking"].date or fields.Datetime.now(), + x["picking"].date_done or fields.Datetime.now(), + x["picking"].name or "", + ), + ) diff --git a/sort_lines_by_product_name/models/product.py b/sort_lines_by_product_name/models/product.py new file mode 100644 index 0000000..55946c8 --- /dev/null +++ b/sort_lines_by_product_name/models/product.py @@ -0,0 +1,13 @@ +# Copyright 2026 Ecocentral, Criptomart +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). +from odoo import models + + +class ProductProduct(models.Model): + _inherit = "product.product" + _order = "name, default_code, id" + + +class ProductTemplate(models.Model): + _inherit = "product.template" + _order = "name, default_code, id" diff --git a/sort_lines_by_product_name/models/purchase_order_line.py b/sort_lines_by_product_name/models/purchase_order_line.py new file mode 100644 index 0000000..423e2bd --- /dev/null +++ b/sort_lines_by_product_name/models/purchase_order_line.py @@ -0,0 +1,17 @@ +# Copyright 2026 Ecocentral, Criptomart +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). +from odoo import fields +from odoo import models + + +class PurchaseOrderLine(models.Model): + _inherit = "purchase.order.line" + _order = "product_name" + + product_name = fields.Char( + string="Nom del producte", + related="product_id.name", + readonly=True, + store=True, + translate=False, + ) diff --git a/sort_lines_by_product_name/models/sale_order_line.py b/sort_lines_by_product_name/models/sale_order_line.py new file mode 100644 index 0000000..868069a --- /dev/null +++ b/sort_lines_by_product_name/models/sale_order_line.py @@ -0,0 +1,17 @@ +# Copyright 2026 Ecocentral, Criptomart +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). +from odoo import fields +from odoo import models + + +class SaleOrderLine(models.Model): + _inherit = "sale.order.line" + _order = "product_name" + + product_name = fields.Char( + string="Nom del producte", + related="product_id.name", + readonly=True, + store=True, + translate=False, + ) diff --git a/sort_lines_by_product_name/models/stock_move_line.py b/sort_lines_by_product_name/models/stock_move_line.py new file mode 100644 index 0000000..94f1d2b --- /dev/null +++ b/sort_lines_by_product_name/models/stock_move_line.py @@ -0,0 +1,17 @@ +# Copyright 2026 Ecocentral, Criptomart +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). +from odoo import fields +from odoo import models + + +class StockMoveLine(models.Model): + _inherit = "stock.move.line" + _order = "product_name" + + product_name = fields.Char( + string="Nom del producte", + related="product_id.name", + readonly=True, + store=True, + translate=False, + ) diff --git a/sort_lines_by_product_name/readme/DESCRIPTION.rst b/sort_lines_by_product_name/readme/DESCRIPTION.rst new file mode 100644 index 0000000..1e16934 --- /dev/null +++ b/sort_lines_by_product_name/readme/DESCRIPTION.rst @@ -0,0 +1,9 @@ +Ordena per nom de producte (en lloc de per ordre d'inserció/referència +interna) les línies de comanda de venda, de compra, de moviment +d'estoc, i el producte mateix. Afegeix també el desempat per nom de +picking a l'agrupació per picking d'``account_invoice_report_grouped_by_picking`` +(OCA) quan dos pickings comparteixen data i data de tancament. + +Port 1:1 de l'``sort_lines_by_product_name`` de la 14. Verificat (fase 7, +tasca 1e) que ``account_invoice_report_grouped_by_picking`` existeix a la +branca 18.0 d'``OCA/account-invoice-reporting`` (18.0.1.0.3). diff --git a/sort_lines_by_product_name/tests/__init__.py b/sort_lines_by_product_name/tests/__init__.py new file mode 100644 index 0000000..d2adf9e --- /dev/null +++ b/sort_lines_by_product_name/tests/__init__.py @@ -0,0 +1 @@ +from . import test_sort_lines_by_product_name diff --git a/sort_lines_by_product_name/tests/test_sort_lines_by_product_name.py b/sort_lines_by_product_name/tests/test_sort_lines_by_product_name.py new file mode 100644 index 0000000..0d41341 --- /dev/null +++ b/sort_lines_by_product_name/tests/test_sort_lines_by_product_name.py @@ -0,0 +1,85 @@ +# Copyright 2026 Ecocentral, Criptomart +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). +from datetime import datetime + +from odoo.tests.common import TransactionCase +from odoo.tests.common import tagged + + +@tagged("post_install", "-at_install") +class TestSortLinesByProductName(TransactionCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.product_z = cls.env["product.product"].create({"name": "Zucchini"}) + cls.product_a = cls.env["product.product"].create({"name": "Amanida"}) + cls.vendor = cls.env["res.partner"].create({"name": "Proveïdor Test"}) + cls.customer = cls.env["res.partner"].create({"name": "Client Test"}) + + def test_purchase_order_line_ordered_by_product_name(self): + order = self.env["purchase.order"].create( + { + "partner_id": self.vendor.id, + "order_line": [ + (0, 0, {"product_id": self.product_z.id, "product_qty": 1.0}), + (0, 0, {"product_id": self.product_a.id, "product_qty": 1.0}), + ], + } + ) + lines = self.env["purchase.order.line"].search([("order_id", "=", order.id)]) + self.assertEqual(lines.mapped("product_id.name"), ["Amanida", "Zucchini"]) + + def test_sale_order_line_ordered_by_product_name(self): + order = self.env["sale.order"].create( + { + "partner_id": self.customer.id, + "order_line": [ + (0, 0, {"product_id": self.product_z.id, "product_uom_qty": 1.0}), + (0, 0, {"product_id": self.product_a.id, "product_uom_qty": 1.0}), + ], + } + ) + lines = self.env["sale.order.line"].search([("order_id", "=", order.id)]) + self.assertEqual(lines.mapped("product_id.name"), ["Amanida", "Zucchini"]) + + def test_product_search_ordered_by_name(self): + products = self.env["product.product"].search( + [("id", "in", [self.product_z.id, self.product_a.id])] + ) + self.assertEqual(list(products), [self.product_a, self.product_z]) + + def test_sort_grouped_lines_breaks_tie_on_picking_name(self): + same_date = datetime(2026, 6, 10, 8, 0, 0) + warehouse = self.env["stock.warehouse"].search( + [("company_id", "=", self.env.company.id)], limit=1 + ) + picking_b = self.env["stock.picking"].create( + { + "picking_type_id": warehouse.out_type_id.id, + "location_id": warehouse.lot_stock_id.id, + "location_dest_id": self.env.ref("stock.stock_location_customers").id, + "date": same_date, + "date_done": same_date, + } + ) + picking_a = self.env["stock.picking"].create( + { + "picking_type_id": warehouse.out_type_id.id, + "location_id": warehouse.lot_stock_id.id, + "location_dest_id": self.env.ref("stock.stock_location_customers").id, + "date": same_date, + "date_done": same_date, + } + ) + # Force deterministic names to make the tie-break assertion exact. + (picking_a + picking_b).flush_recordset() + picking_a.name, picking_b.name = "WH/OUT/A", "WH/OUT/B" + + lines_dic = [{"picking": picking_b}, {"picking": picking_a}] + result = self.env["account.move"]._sort_grouped_lines(lines_dic) + self.assertEqual([r["picking"] for r in result], [picking_a, picking_b]) + + def test_sort_grouped_lines_handles_missing_dates(self): + picking = self.env["stock.picking"].new({"name": "WH/OUT/999"}) + # Must not raise even without date/date_done set. + self.env["account.move"]._sort_grouped_lines([{"picking": picking}]) diff --git a/stock_picking_report_undelivered_product/__init__.py b/stock_picking_report_undelivered_product/__init__.py new file mode 100644 index 0000000..0650744 --- /dev/null +++ b/stock_picking_report_undelivered_product/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/stock_picking_report_undelivered_product/__manifest__.py b/stock_picking_report_undelivered_product/__manifest__.py new file mode 100644 index 0000000..18b449d --- /dev/null +++ b/stock_picking_report_undelivered_product/__manifest__.py @@ -0,0 +1,22 @@ +# Copyright 2020 Tecnativa - Sergio Teruel +# Copyright 2026 Ecocentral, Criptomart +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). +{ + "name": "Stock Picking Report Undelivered Product", + "summary": "Display undelivered product lines on the delivery slip report", + "version": "18.0.1.0.0", + "development_status": "Beta", + "author": "Tecnativa, Ecocentral, Criptomart, Odoo Community Association (OCA)", + "website": "https://github.com/Ecocentral/ecocentral", + "category": "Warehouse", + "license": "AGPL-3", + "depends": [ + "stock", + ], + "data": [ + "views/product_views.xml", + "views/res_partner_views.xml", + "views/res_config_settings_views.xml", + "views/report_deliveryslip.xml", + ], +} diff --git a/stock_picking_report_undelivered_product/models/__init__.py b/stock_picking_report_undelivered_product/models/__init__.py new file mode 100644 index 0000000..d021699 --- /dev/null +++ b/stock_picking_report_undelivered_product/models/__init__.py @@ -0,0 +1,5 @@ +from . import product_template +from . import res_config_settings +from . import res_partner +from . import stock_move +from . import stock_move_line diff --git a/stock_picking_report_undelivered_product/models/product_template.py b/stock_picking_report_undelivered_product/models/product_template.py new file mode 100644 index 0000000..b24916d --- /dev/null +++ b/stock_picking_report_undelivered_product/models/product_template.py @@ -0,0 +1,10 @@ +# Copyright 2020 Tecnativa - Sergio Teruel +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). +from odoo import fields +from odoo import models + + +class ProductTemplate(models.Model): + _inherit = "product.template" + + display_undelivered_in_picking = fields.Boolean(default=True) diff --git a/stock_picking_report_undelivered_product/models/res_config_settings.py b/stock_picking_report_undelivered_product/models/res_config_settings.py new file mode 100644 index 0000000..4a686cc --- /dev/null +++ b/stock_picking_report_undelivered_product/models/res_config_settings.py @@ -0,0 +1,33 @@ +# Copyright 2020 Tecnativa - Sergio Teruel +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). +from odoo import fields +from odoo import models + + +class ResConfigSettings(models.TransientModel): + _inherit = "res.config.settings" + + undelivered_product_slip_report_method = fields.Selection( + related="company_id.undelivered_product_slip_report_method", + readonly=False, + ) + + +class ResCompany(models.Model): + _inherit = "res.company" + + undelivered_product_slip_report_method = fields.Selection( + [ + ("all", "Display all undelivered product lines"), + ( + "partially_undelivered", + "Display only partially undelivered product lines", + ), + ( + "completely_undelivered", + "Display only completely undelivered product lines", + ), + ], + string="Method to display undelivered product lines in report picking", + default="all", + ) diff --git a/stock_picking_report_undelivered_product/models/res_partner.py b/stock_picking_report_undelivered_product/models/res_partner.py new file mode 100644 index 0000000..a485739 --- /dev/null +++ b/stock_picking_report_undelivered_product/models/res_partner.py @@ -0,0 +1,10 @@ +# Copyright 2020 Tecnativa - Sergio Teruel +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). +from odoo import fields +from odoo import models + + +class ResPartner(models.Model): + _inherit = "res.partner" + + display_undelivered_in_picking = fields.Boolean(default=True) diff --git a/stock_picking_report_undelivered_product/models/stock_move.py b/stock_picking_report_undelivered_product/models/stock_move.py new file mode 100644 index 0000000..a4aeedb --- /dev/null +++ b/stock_picking_report_undelivered_product/models/stock_move.py @@ -0,0 +1,18 @@ +# Copyright 2020 Tecnativa - Sergio Teruel +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). +from odoo import fields +from odoo import models + + +class StockMove(models.Model): + _inherit = "stock.move" + + splitted_stock_move_orig_id = fields.Many2one( + comodel_name="stock.move", string="Splitted from", readonly=True + ) + + def _prepare_move_split_vals(self, qty): + """Store origin stock move which created the splitted move.""" + vals = super()._prepare_move_split_vals(qty) + vals["splitted_stock_move_orig_id"] = self.id + return vals diff --git a/stock_picking_report_undelivered_product/models/stock_move_line.py b/stock_picking_report_undelivered_product/models/stock_move_line.py new file mode 100644 index 0000000..b4c4b38 --- /dev/null +++ b/stock_picking_report_undelivered_product/models/stock_move_line.py @@ -0,0 +1,23 @@ +# Copyright 2022 Tecnativa - Sergio Teruel +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). +from odoo import models + + +class StockMoveLine(models.Model): + _inherit = "stock.move.line" + + def _get_aggregated_product_quantities(self, **kwargs): + """Odoo displays undelivered products in the main table too. + + Remove them from the aggregated dict so they only show up in the + bottom "remaining products" table. + """ + aggregated_move_lines = super()._get_aggregated_product_quantities(**kwargs) + keys_to_remove = { + key + for key, values in aggregated_move_lines.items() + if not values["quantity"] + } + for key in keys_to_remove: + aggregated_move_lines.pop(key, None) + return aggregated_move_lines diff --git a/stock_picking_report_undelivered_product/readme/DESCRIPTION.rst b/stock_picking_report_undelivered_product/readme/DESCRIPTION.rst new file mode 100644 index 0000000..0107451 --- /dev/null +++ b/stock_picking_report_undelivered_product/readme/DESCRIPTION.rst @@ -0,0 +1,8 @@ +Muestra en el informe de albarán (delivery slip) las líneas de producto que +no se han servido: aquellas cuyo ``stock.move`` acaba cancelado (sin +backorder) porque el producto pedido no llegó a entregarse, total o +parcialmente. + +Configurable por partner y por producto (casilla "Mostrar no servidos en el +albarán", activada por defecto), y por compañía (mostrar todas las líneas no +servidas, solo las parcialmente servidas, o solo las totalmente no servidas). diff --git a/stock_picking_report_undelivered_product/readme/USAGE.rst b/stock_picking_report_undelivered_product/readme/USAGE.rst new file mode 100644 index 0000000..8991f96 --- /dev/null +++ b/stock_picking_report_undelivered_product/readme/USAGE.rst @@ -0,0 +1,5 @@ +En Inventario > Configuración > Ajustes, sección "Report picking undelivered +products", elegir el método (todas / solo parciales / solo completas). + +En la ficha del partner o del producto, casilla "Display Undelivered In +Picking" para excluirlo del bloque de no servidos. diff --git a/stock_picking_report_undelivered_product/tests/__init__.py b/stock_picking_report_undelivered_product/tests/__init__.py new file mode 100644 index 0000000..f6c20ce --- /dev/null +++ b/stock_picking_report_undelivered_product/tests/__init__.py @@ -0,0 +1 @@ +from . import test_stock_picking_report_undelivered_product diff --git a/stock_picking_report_undelivered_product/tests/test_stock_picking_report_undelivered_product.py b/stock_picking_report_undelivered_product/tests/test_stock_picking_report_undelivered_product.py new file mode 100644 index 0000000..42230bc --- /dev/null +++ b/stock_picking_report_undelivered_product/tests/test_stock_picking_report_undelivered_product.py @@ -0,0 +1,188 @@ +# Copyright 2020 Tecnativa - Sergio Teruel +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). +from odoo.tests import Form +from odoo.tests import TransactionCase + + +class TestStockPickingReportUndeliveredProduct(TransactionCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.ResPartner = cls.env["res.partner"] + cls.ProductProduct = cls.env["product.product"] + cls.StockPicking = cls.env["stock.picking"] + cls.StockQuant = cls.env["stock.quant"] + cls.BackOrderWiz = cls.env["stock.backorder.confirmation"] + cls.warehouse = cls.env.ref("stock.warehouse0") + cls.picking_type_out = cls.env.ref("stock.picking_type_out") + + cls.partner_display = cls.ResPartner.create( + {"name": "Partner for test display", "display_undelivered_in_picking": True} + ) + cls.partner_no_display = cls.ResPartner.create( + { + "name": "Partner for test on display", + "display_undelivered_in_picking": False, + } + ) + + cls.product_display = cls.ProductProduct.create( + { + "name": "Test product undelivered display", + "display_undelivered_in_picking": True, + "type": "consu", + "is_storable": True, + } + ) + cls.product_no_display = cls.ProductProduct.create( + { + "name": "Test product undelivered no display", + "display_undelivered_in_picking": False, + "type": "consu", + "is_storable": True, + } + ) + cls.product_no_display_wo_stock = cls.ProductProduct.create( + { + "name": "Test product undelivered no display without stock", + "display_undelivered_in_picking": False, + "type": "consu", + "is_storable": True, + } + ) + cls.StockQuant.create( + { + "product_id": cls.product_no_display.id, + "location_id": cls.warehouse.lot_stock_id.id, + "quantity": 2000, + } + ) + + def _create_picking(self, partner): + picking_form = Form(self.StockPicking) + picking_form.picking_type_id = self.picking_type_out + picking_form.partner_id = partner + + with picking_form.move_ids_without_package.new() as line: + line.product_id = self.product_display + line.product_uom_qty = 50.00 + with picking_form.move_ids_without_package.new() as line: + line.product_id = self.product_no_display + line.product_uom_qty = 20.00 + with picking_form.move_ids_without_package.new() as line: + line.product_id = self.product_no_display_wo_stock + line.product_uom_qty = 20.00 + return picking_form.save() + + def _transfer_picking_no_backorder(self, picking): + # Transfer picking with no create backorder option + picking.move_line_ids.picked = True + backorder_wizard = self.BackOrderWiz.create({"pick_ids": [(4, picking.id)]}) + backorder_wizard.with_context( + button_validate_picking_ids=picking.id + ).process_cancel_backorder() + + def _render(self, picking): + return self.env["ir.actions.report"]._render_qweb_html( + "stock.report_deliveryslip", picking.ids + ) + + def test_displayed_customer(self): + picking = self._create_picking(self.partner_display) + picking.action_confirm() + picking.action_assign() + picking.move_line_ids.filtered( + lambda ml: ml.product_id == self.product_display + ).quantity = 10.00 + self._transfer_picking_no_backorder(picking) + res = self._render(picking) + self.assertIn("undelivered_product", str(res[0])) + + def test_no_displayed_customer(self): + picking = self._create_picking(self.partner_no_display) + picking.action_confirm() + picking.action_assign() + picking.move_line_ids.filtered( + lambda ml: ml.product_id == self.product_display + ).quantity = 10.00 + self._transfer_picking_no_backorder(picking) + res = self._render(picking) + self.assertNotIn("undelivered_product", str(res[0])) + + def test_no_displayed_product(self): + picking = self._create_picking(self.partner_display) + picking.move_ids.filtered( + lambda move: move.product_id == self.product_display + ).unlink() + picking.action_confirm() + picking.action_assign() + picking.move_line_ids.quantity = 10.00 + self._transfer_picking_no_backorder(picking) + res = self._render(picking) + self.assertNotIn("undelivered_product", str(res[0])) + + def test_picking_report_method(self): + product = self.ProductProduct.create( + { + "name": "test01", + "display_undelivered_in_picking": True, + "type": "consu", + "is_storable": True, + } + ) + product2 = self.ProductProduct.create( + { + "name": "test02", + "display_undelivered_in_picking": True, + "type": "consu", + "is_storable": True, + } + ) + self.StockQuant.create( + { + "product_id": product.id, + "location_id": self.warehouse.lot_stock_id.id, + "quantity": 2000, + } + ) + picking_form = Form(self.StockPicking) + picking_form.picking_type_id = self.picking_type_out + picking_form.partner_id = self.partner_display + with picking_form.move_ids_without_package.new() as line: + line.product_id = product + line.product_uom_qty = 50.00 + with picking_form.move_ids_without_package.new() as line: + line.product_id = product2 + line.product_uom_qty = 20.00 + picking = picking_form.save() + + picking.action_confirm() + picking.action_assign() + picking.move_line_ids.filtered(lambda ml: ml.product_id == product).quantity = ( + 10.00 + ) + self._transfer_picking_no_backorder(picking) + + # Empty setting method field + picking.company_id.undelivered_product_slip_report_method = False + res = self._render(picking) + self.assertIn("test02", str(res[0])) + + # Print all undelivered lines, partial and completely lines + picking.company_id.undelivered_product_slip_report_method = "all" + res = self._render(picking) + self.assertIn("test02", str(res[0])) + + # Print only partial undelivered lines + picking.company_id.undelivered_product_slip_report_method = ( + "partially_undelivered" + ) + res = self._render(picking) + self.assertNotIn("test02", str(res[0])) + + # Print only completely undelivered lines + picking.company_id.undelivered_product_slip_report_method = ( + "completely_undelivered" + ) + res = self._render(picking) + self.assertNotIn("partially_undelivered_line", str(res[0])) diff --git a/stock_picking_report_undelivered_product/views/product_views.xml b/stock_picking_report_undelivered_product/views/product_views.xml new file mode 100644 index 0000000..9a43c24 --- /dev/null +++ b/stock_picking_report_undelivered_product/views/product_views.xml @@ -0,0 +1,17 @@ + + + + + product.template.form.inherit.undelivered.product + product.template + + + + + + + + + diff --git a/stock_picking_report_undelivered_product/views/report_deliveryslip.xml b/stock_picking_report_undelivered_product/views/report_deliveryslip.xml new file mode 100644 index 0000000..ad7679b --- /dev/null +++ b/stock_picking_report_undelivered_product/views/report_deliveryslip.xml @@ -0,0 +1,92 @@ + + + + + + + + diff --git a/stock_picking_report_undelivered_product/views/res_config_settings_views.xml b/stock_picking_report_undelivered_product/views/res_config_settings_views.xml new file mode 100644 index 0000000..0c5bd83 --- /dev/null +++ b/stock_picking_report_undelivered_product/views/res_config_settings_views.xml @@ -0,0 +1,25 @@ + + + + res.config.settings.view.form.inherit.undelivered.product + res.config.settings + + + + + + + + + + diff --git a/stock_picking_report_undelivered_product/views/res_partner_views.xml b/stock_picking_report_undelivered_product/views/res_partner_views.xml new file mode 100644 index 0000000..900a633 --- /dev/null +++ b/stock_picking_report_undelivered_product/views/res_partner_views.xml @@ -0,0 +1,15 @@ + + + + + res.partner.form.inherit.undelivered.product + res.partner + + + + + + + + +