addons portados en el repo de ecocentral

This commit is contained in:
GitHub Copilot 2026-08-07 16:34:38 +02:00
parent f2194c5367
commit 65818b0155
72 changed files with 2474 additions and 3 deletions

View file

@ -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

View file

@ -0,0 +1,3 @@
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html).
from . import models

View file

@ -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",
],
}

View file

@ -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 (`<model>_view_form_<mòdul>`). 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
)

View file

@ -0,0 +1,3 @@
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html).
from . import res_partner

View file

@ -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.",
)

View file

@ -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``.

View file

@ -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.

View file

@ -0,0 +1,3 @@
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html).
from . import test_res_partner_ccpae

View file

@ -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)

View file

@ -0,0 +1,22 @@
<?xml version="1.0" encoding="utf-8" ?>
<!--
Copyright 2026 Ecocentral, Criptomart
License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html).
-->
<odoo>
<record id="res_partner_view_form_ccpae" model="ir.ui.view">
<field name="name">res.partner.form.ccpae</field>
<field name="model">res.partner</field>
<field name="inherit_id" ref="base.view_partner_form" />
<field name="arch" type="xml">
<xpath expr="//page[@name='sales_purchases']" position="after">
<page name="ccpae" string="CCPAE" invisible="not is_company">
<group name="ccpae_group">
<field name="ccpae_operador" />
<field name="ccpae_organismo" />
</group>
</page>
</xpath>
</field>
</record>
</odoo>

View file

@ -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 <https://odoo-community.org/page/development-status>`_
**Table of contents**
.. contents::
:local:
Bug Tracker
===========
Bugs are tracked on `GitHub Issues <https://github.com/OCA/pos/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 <https://github.com/OCA/pos/issues/new?body=module:%20pos_full_refund%0Aversion:%2018.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**>`_.
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 <https://odoo-community.org/page/maintainer-role>`__:
|maintainer-LorenzoC0|
This module is part of the `OCA/pos <https://github.com/OCA/pos/tree/18.0/pos_full_refund>`_ project on GitHub.
You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

View file

@ -0,0 +1 @@
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).

View file

@ -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",
],
},
}

View file

@ -0,0 +1,6 @@
- [Innovyou] (https://www.innovyou.it):
- Lorenzo Carta
- Lorenzo Battistini
- Valerio Paretta
- [Criptomart](https://criptomart.net):
- Migration to 18.0

View file

@ -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.

View file

@ -0,0 +1,440 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta name="generator" content="Docutils: https://docutils.sourceforge.io/" />
<title>Point of Sale - Full Refund</title>
<style type="text/css">
/*
:Author: David Goodger (goodger@python.org)
:Id: $Id: html4css1.css 9511 2024-01-13 09:50:07Z milde $
:Copyright: This stylesheet has been placed in the public domain.
Default cascading style sheet for the HTML output of Docutils.
Despite the name, some widely supported CSS2 features are used.
See https://docutils.sourceforge.io/docs/howto/html-stylesheets.html for how to
customize this style sheet.
*/
/* used to remove borders from tables and images */
.borderless, table.borderless td, table.borderless th {
border: 0 }
table.borderless td, table.borderless th {
/* Override padding for "table.docutils td" with "! important".
The right padding separates the table cells. */
padding: 0 0.5em 0 0 ! important }
.first {
/* Override more specific margin styles with "! important". */
margin-top: 0 ! important }
.last, .with-subtitle {
margin-bottom: 0 ! important }
.hidden {
display: none }
.subscript {
vertical-align: sub;
font-size: smaller }
.superscript {
vertical-align: super;
font-size: smaller }
a.toc-backref {
text-decoration: none ;
color: black }
blockquote.epigraph {
margin: 2em 5em ; }
dl.docutils dd {
margin-bottom: 0.5em }
object[type="image/svg+xml"], object[type="application/x-shockwave-flash"] {
overflow: hidden;
}
/* Uncomment (and remove this text!) to get bold-faced definition list terms
dl.docutils dt {
font-weight: bold }
*/
div.abstract {
margin: 2em 5em }
div.abstract p.topic-title {
font-weight: bold ;
text-align: center }
div.admonition, div.attention, div.caution, div.danger, div.error,
div.hint, div.important, div.note, div.tip, div.warning {
margin: 2em ;
border: medium outset ;
padding: 1em }
div.admonition p.admonition-title, div.hint p.admonition-title,
div.important p.admonition-title, div.note p.admonition-title,
div.tip p.admonition-title {
font-weight: bold ;
font-family: sans-serif }
div.attention p.admonition-title, div.caution p.admonition-title,
div.danger p.admonition-title, div.error p.admonition-title,
div.warning p.admonition-title, .code .error {
color: red ;
font-weight: bold ;
font-family: sans-serif }
/* Uncomment (and remove this text!) to get reduced vertical space in
compound paragraphs.
div.compound .compound-first, div.compound .compound-middle {
margin-bottom: 0.5em }
div.compound .compound-last, div.compound .compound-middle {
margin-top: 0.5em }
*/
div.dedication {
margin: 2em 5em ;
text-align: center ;
font-style: italic }
div.dedication p.topic-title {
font-weight: bold ;
font-style: normal }
div.figure {
margin-left: 2em ;
margin-right: 2em }
div.footer, div.header {
clear: both;
font-size: smaller }
div.line-block {
display: block ;
margin-top: 1em ;
margin-bottom: 1em }
div.line-block div.line-block {
margin-top: 0 ;
margin-bottom: 0 ;
margin-left: 1.5em }
div.sidebar {
margin: 0 0 0.5em 1em ;
border: medium outset ;
padding: 1em ;
background-color: #ffffee ;
width: 40% ;
float: right ;
clear: right }
div.sidebar p.rubric {
font-family: sans-serif ;
font-size: medium }
div.system-messages {
margin: 5em }
div.system-messages h1 {
color: red }
div.system-message {
border: medium outset ;
padding: 1em }
div.system-message p.system-message-title {
color: red ;
font-weight: bold }
div.topic {
margin: 2em }
h1.section-subtitle, h2.section-subtitle, h3.section-subtitle,
h4.section-subtitle, h5.section-subtitle, h6.section-subtitle {
margin-top: 0.4em }
h1.title {
text-align: center }
h2.subtitle {
text-align: center }
hr.docutils {
width: 75% }
img.align-left, .figure.align-left, object.align-left, table.align-left {
clear: left ;
float: left ;
margin-right: 1em }
img.align-right, .figure.align-right, object.align-right, table.align-right {
clear: right ;
float: right ;
margin-left: 1em }
img.align-center, .figure.align-center, object.align-center {
display: block;
margin-left: auto;
margin-right: auto;
}
table.align-center {
margin-left: auto;
margin-right: auto;
}
.align-left {
text-align: left }
.align-center {
clear: both ;
text-align: center }
.align-right {
text-align: right }
/* reset inner alignment in figures */
div.align-right {
text-align: inherit }
/* div.align-center * { */
/* text-align: left } */
.align-top {
vertical-align: top }
.align-middle {
vertical-align: middle }
.align-bottom {
vertical-align: bottom }
ol.simple, ul.simple {
margin-bottom: 1em }
ol.arabic {
list-style: decimal }
ol.loweralpha {
list-style: lower-alpha }
ol.upperalpha {
list-style: upper-alpha }
ol.lowerroman {
list-style: lower-roman }
ol.upperroman {
list-style: upper-roman }
p.attribution {
text-align: right ;
margin-left: 50% }
p.caption {
font-style: italic }
p.credits {
font-style: italic ;
font-size: smaller }
p.label {
white-space: nowrap }
p.rubric {
font-weight: bold ;
font-size: larger ;
color: maroon ;
text-align: center }
p.sidebar-title {
font-family: sans-serif ;
font-weight: bold ;
font-size: larger }
p.sidebar-subtitle {
font-family: sans-serif ;
font-weight: bold }
p.topic-title {
font-weight: bold }
pre.address {
margin-bottom: 0 ;
margin-top: 0 ;
font: inherit }
pre.literal-block, pre.doctest-block, pre.math, pre.code {
margin-left: 2em ;
margin-right: 2em }
pre.code .ln { color: gray; } /* line numbers */
pre.code, code { background-color: #eeeeee }
pre.code .comment, code .comment { color: #5C6576 }
pre.code .keyword, code .keyword { color: #3B0D06; font-weight: bold }
pre.code .literal.string, code .literal.string { color: #0C5404 }
pre.code .name.builtin, code .name.builtin { color: #352B84 }
pre.code .deleted, code .deleted { background-color: #DEB0A1}
pre.code .inserted, code .inserted { background-color: #A3D289}
span.classifier {
font-family: sans-serif ;
font-style: oblique }
span.classifier-delimiter {
font-family: sans-serif ;
font-weight: bold }
span.interpreted {
font-family: sans-serif }
span.option {
white-space: nowrap }
span.pre {
white-space: pre }
span.problematic, pre.problematic {
color: red }
span.section-subtitle {
/* font-size relative to parent (h1..h6 element) */
font-size: 80% }
table.citation {
border-left: solid 1px gray;
margin-left: 1px }
table.docinfo {
margin: 2em 4em }
table.docutils {
margin-top: 0.5em ;
margin-bottom: 0.5em }
table.footnote {
border-left: solid 1px black;
margin-left: 1px }
table.docutils td, table.docutils th,
table.docinfo td, table.docinfo th {
padding-left: 0.5em ;
padding-right: 0.5em ;
vertical-align: top }
table.docutils th.field-name, table.docinfo th.docinfo-name {
font-weight: bold ;
text-align: left ;
white-space: nowrap ;
padding-left: 0 }
/* "booktabs" style (no vertical lines) */
table.docutils.booktabs {
border: 0px;
border-top: 2px solid;
border-bottom: 2px solid;
border-collapse: collapse;
}
table.docutils.booktabs * {
border: 0px;
}
table.docutils.booktabs th {
border-bottom: thin solid;
text-align: left;
}
h1 tt.docutils, h2 tt.docutils, h3 tt.docutils,
h4 tt.docutils, h5 tt.docutils, h6 tt.docutils {
font-size: 100% }
ul.auto-toc {
list-style-type: none }
</style>
</head>
<body>
<div class="document" id="point-of-sale">
<h1 class="title">Point of Sale - Full Refund</h1>
<!-- !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! This file is generated by oca-gen-addon-readme !!
!! changes will be overwritten. !!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! source digest: sha256:cc92aeed4d5986a6c3a0e7860d32d67c2aceec105975c3a9ffca2caab7ae128f
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -->
<p><a class="reference external image-reference" href="https://odoo-community.org/page/development-status"><img alt="Alpha" src="https://img.shields.io/badge/maturity-Alpha-red.png" /></a> <a class="reference external image-reference" href="http://www.gnu.org/licenses/agpl-3.0-standalone.html"><img alt="License: AGPL-3" src="https://img.shields.io/badge/licence-AGPL--3-blue.png" /></a> <a class="reference external image-reference" href="https://github.com/OCA/pos/tree/18.0/pos_full_refund"><img alt="OCA/pos" src="https://img.shields.io/badge/github-OCA%2Fpos-lightgray.png?logo=github" /></a> <a class="reference external image-reference" href="https://translation.odoo-community.org/projects/pos-18-0/pos-18-0-pos_full_refund"><img alt="Translate me on Weblate" src="https://img.shields.io/badge/weblate-Translate%20me-F47D42.png" /></a> <a class="reference external image-reference" href="https://runboat.odoo-community.org/builds?repo=OCA/pos&amp;target_branch=18.0"><img alt="Try me on Runboat" src="https://img.shields.io/badge/runboat-Try%20me-875A7B.png" /></a></p>
<p>This module adds a <strong>Do Full Refund</strong> 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.</p>
<div class="admonition important">
<p class="first admonition-title">Important</p>
<p class="last">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.
<a class="reference external" href="https://odoo-community.org/page/development-status">More details on development status</a></p>
</div>
<p><strong>Table of contents</strong></p>
<div class="contents local topic" id="contents">
<ul class="simple">
<li><a class="reference internal" href="#bug-tracker" id="toc-entry-1">Bug Tracker</a></li>
<li><a class="reference internal" href="#credits" id="toc-entry-2">Credits</a><ul>
<li><a class="reference internal" href="#authors" id="toc-entry-3">Authors</a></li>
<li><a class="reference internal" href="#contributors" id="toc-entry-4">Contributors</a></li>
<li><a class="reference internal" href="#maintainers" id="toc-entry-5">Maintainers</a></li>
</ul>
</li>
</ul>
</div>
<div class="section" id="bug-tracker">
<h1><a class="toc-backref" href="#toc-entry-1">Bug Tracker</a></h1>
<p>Bugs are tracked on <a class="reference external" href="https://github.com/OCA/pos/issues">GitHub Issues</a>.
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
<a class="reference external" href="https://github.com/OCA/pos/issues/new?body=module:%20pos_full_refund%0Aversion:%2018.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**">feedback</a>.</p>
<p>Do not contact contributors directly about support or help with technical issues.</p>
</div>
<div class="section" id="credits">
<h1><a class="toc-backref" href="#toc-entry-2">Credits</a></h1>
<div class="section" id="authors">
<h2><a class="toc-backref" href="#toc-entry-3">Authors</a></h2>
<ul class="simple">
<li>Innovyou</li>
</ul>
</div>
<div class="section" id="contributors">
<h2><a class="toc-backref" href="#toc-entry-4">Contributors</a></h2>
<ul class="simple">
<li>[Innovyou] (<a class="reference external" href="https://www.innovyou.it">https://www.innovyou.it</a>):<ul>
<li>Lorenzo Carta</li>
<li>Lorenzo Battistini</li>
<li>Valerio Paretta</li>
</ul>
</li>
</ul>
</div>
<div class="section" id="maintainers">
<h2><a class="toc-backref" href="#toc-entry-5">Maintainers</a></h2>
<p>This module is maintained by the OCA.</p>
<a class="reference external image-reference" href="https://odoo-community.org">
<img alt="Odoo Community Association" src="https://odoo-community.org/logo.png" />
</a>
<p>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.</p>
<p>Current <a class="reference external" href="https://odoo-community.org/page/maintainer-role">maintainer</a>:</p>
<p><a class="reference external image-reference" href="https://github.com/LorenzoC0"><img alt="LorenzoC0" src="https://github.com/LorenzoC0.png?size=40px" /></a></p>
<p>This module is part of the <a class="reference external" href="https://github.com/OCA/pos/tree/18.0/pos_full_refund">OCA/pos</a> project on GitHub.</p>
<p>You are welcome to contribute. To learn how please visit <a class="reference external" href="https://odoo-community.org/page/Contribute">https://odoo-community.org/page/Contribute</a>.</p>
</div>
</div>
</div>
</body>
</html>

View file

@ -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();
},
});

View file

@ -0,0 +1,21 @@
<?xml version="1.0" encoding="UTF-8" ?>
<templates id="template" xml:space="preserve">
<t
t-name="pos_full_refund.TicketScreen"
t-inherit="point_of_sale.TicketScreen"
t-inherit-mode="extension"
>
<xpath expr="//InvoiceButton" position="before">
<button
id="set_full_refund_button"
class="control-button btn btn-light btn-lg lh-lg flex-grow-1 flex-shrink-1"
t-on-click="() => this.onDoFullRefund()"
>
<i class="fa fa-cart-arrow-down me-1" />
Do Full Refund
</button>
</xpath>
</t>
</templates>

View file

@ -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(),
});

View file

@ -0,0 +1,3 @@
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from . import test_pos_full_refund

View file

@ -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")

View file

@ -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 <https://github.com/OCA/pos/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 <https://github.com/OCA/pos/issues/new?body=module:%20pos_order_reorder%0Aversion:%2018.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**>`_.
Do not contact contributors directly about support or help with technical issues.
Credits
=======
Authors
-------
* Cetmix
Contributors
------------
- Cetmix <https://cetmix.com/>
- Dinar Gabbasov
- `Heliconia Solutions Pvt. Ltd. <https://www.heliconia.io>`__
- 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 <https://github.com/OCA/pos/tree/18.0/pos_order_reorder>`_ project on GitHub.
You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

View file

@ -0,0 +1 @@
from . import models

View file

@ -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",
}

View file

@ -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é <lb.patri@gmail.com>\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"

View file

@ -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 <elodie@comptoirdecampagne.fr>\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"

View file

@ -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 <stefano.consolaro@mymage.it>\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"

View file

@ -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 ""

View file

@ -0,0 +1,2 @@
from . import pos_config
from . import res_config_settings

View file

@ -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)

View file

@ -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
)

View file

@ -0,0 +1,3 @@
[build-system]
requires = ["whool"]
build-backend = "whool.buildapi"

View file

@ -0,0 +1 @@
Select PoS \> Configuration \> Settings \> enable flag "Allow Reorder"

View file

@ -0,0 +1,4 @@
- Cetmix \<<https://cetmix.com/>\>
- Dinar Gabbasov
- [Heliconia Solutions Pvt. Ltd.](https://www.heliconia.io)
- Bhavesh Heliconia

View file

@ -0,0 +1,2 @@
This module allows you to re-order a paid order on a new order:
![image](../static/img/reorder_button.png)

Binary file not shown.

After

Width:  |  Height:  |  Size: 104 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.2 KiB

View file

@ -0,0 +1,436 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta name="generator" content="Docutils: https://docutils.sourceforge.io/" />
<title>Point of Sale Re-order</title>
<style type="text/css">
/*
:Author: David Goodger (goodger@python.org)
:Id: $Id: html4css1.css 9511 2024-01-13 09:50:07Z milde $
:Copyright: This stylesheet has been placed in the public domain.
Default cascading style sheet for the HTML output of Docutils.
Despite the name, some widely supported CSS2 features are used.
See https://docutils.sourceforge.io/docs/howto/html-stylesheets.html for how to
customize this style sheet.
*/
/* used to remove borders from tables and images */
.borderless, table.borderless td, table.borderless th {
border: 0 }
table.borderless td, table.borderless th {
/* Override padding for "table.docutils td" with "! important".
The right padding separates the table cells. */
padding: 0 0.5em 0 0 ! important }
.first {
/* Override more specific margin styles with "! important". */
margin-top: 0 ! important }
.last, .with-subtitle {
margin-bottom: 0 ! important }
.hidden {
display: none }
.subscript {
vertical-align: sub;
font-size: smaller }
.superscript {
vertical-align: super;
font-size: smaller }
a.toc-backref {
text-decoration: none ;
color: black }
blockquote.epigraph {
margin: 2em 5em ; }
dl.docutils dd {
margin-bottom: 0.5em }
object[type="image/svg+xml"], object[type="application/x-shockwave-flash"] {
overflow: hidden;
}
/* Uncomment (and remove this text!) to get bold-faced definition list terms
dl.docutils dt {
font-weight: bold }
*/
div.abstract {
margin: 2em 5em }
div.abstract p.topic-title {
font-weight: bold ;
text-align: center }
div.admonition, div.attention, div.caution, div.danger, div.error,
div.hint, div.important, div.note, div.tip, div.warning {
margin: 2em ;
border: medium outset ;
padding: 1em }
div.admonition p.admonition-title, div.hint p.admonition-title,
div.important p.admonition-title, div.note p.admonition-title,
div.tip p.admonition-title {
font-weight: bold ;
font-family: sans-serif }
div.attention p.admonition-title, div.caution p.admonition-title,
div.danger p.admonition-title, div.error p.admonition-title,
div.warning p.admonition-title, .code .error {
color: red ;
font-weight: bold ;
font-family: sans-serif }
/* Uncomment (and remove this text!) to get reduced vertical space in
compound paragraphs.
div.compound .compound-first, div.compound .compound-middle {
margin-bottom: 0.5em }
div.compound .compound-last, div.compound .compound-middle {
margin-top: 0.5em }
*/
div.dedication {
margin: 2em 5em ;
text-align: center ;
font-style: italic }
div.dedication p.topic-title {
font-weight: bold ;
font-style: normal }
div.figure {
margin-left: 2em ;
margin-right: 2em }
div.footer, div.header {
clear: both;
font-size: smaller }
div.line-block {
display: block ;
margin-top: 1em ;
margin-bottom: 1em }
div.line-block div.line-block {
margin-top: 0 ;
margin-bottom: 0 ;
margin-left: 1.5em }
div.sidebar {
margin: 0 0 0.5em 1em ;
border: medium outset ;
padding: 1em ;
background-color: #ffffee ;
width: 40% ;
float: right ;
clear: right }
div.sidebar p.rubric {
font-family: sans-serif ;
font-size: medium }
div.system-messages {
margin: 5em }
div.system-messages h1 {
color: red }
div.system-message {
border: medium outset ;
padding: 1em }
div.system-message p.system-message-title {
color: red ;
font-weight: bold }
div.topic {
margin: 2em }
h1.section-subtitle, h2.section-subtitle, h3.section-subtitle,
h4.section-subtitle, h5.section-subtitle, h6.section-subtitle {
margin-top: 0.4em }
h1.title {
text-align: center }
h2.subtitle {
text-align: center }
hr.docutils {
width: 75% }
img.align-left, .figure.align-left, object.align-left, table.align-left {
clear: left ;
float: left ;
margin-right: 1em }
img.align-right, .figure.align-right, object.align-right, table.align-right {
clear: right ;
float: right ;
margin-left: 1em }
img.align-center, .figure.align-center, object.align-center {
display: block;
margin-left: auto;
margin-right: auto;
}
table.align-center {
margin-left: auto;
margin-right: auto;
}
.align-left {
text-align: left }
.align-center {
clear: both ;
text-align: center }
.align-right {
text-align: right }
/* reset inner alignment in figures */
div.align-right {
text-align: inherit }
/* div.align-center * { */
/* text-align: left } */
.align-top {
vertical-align: top }
.align-middle {
vertical-align: middle }
.align-bottom {
vertical-align: bottom }
ol.simple, ul.simple {
margin-bottom: 1em }
ol.arabic {
list-style: decimal }
ol.loweralpha {
list-style: lower-alpha }
ol.upperalpha {
list-style: upper-alpha }
ol.lowerroman {
list-style: lower-roman }
ol.upperroman {
list-style: upper-roman }
p.attribution {
text-align: right ;
margin-left: 50% }
p.caption {
font-style: italic }
p.credits {
font-style: italic ;
font-size: smaller }
p.label {
white-space: nowrap }
p.rubric {
font-weight: bold ;
font-size: larger ;
color: maroon ;
text-align: center }
p.sidebar-title {
font-family: sans-serif ;
font-weight: bold ;
font-size: larger }
p.sidebar-subtitle {
font-family: sans-serif ;
font-weight: bold }
p.topic-title {
font-weight: bold }
pre.address {
margin-bottom: 0 ;
margin-top: 0 ;
font: inherit }
pre.literal-block, pre.doctest-block, pre.math, pre.code {
margin-left: 2em ;
margin-right: 2em }
pre.code .ln { color: gray; } /* line numbers */
pre.code, code { background-color: #eeeeee }
pre.code .comment, code .comment { color: #5C6576 }
pre.code .keyword, code .keyword { color: #3B0D06; font-weight: bold }
pre.code .literal.string, code .literal.string { color: #0C5404 }
pre.code .name.builtin, code .name.builtin { color: #352B84 }
pre.code .deleted, code .deleted { background-color: #DEB0A1}
pre.code .inserted, code .inserted { background-color: #A3D289}
span.classifier {
font-family: sans-serif ;
font-style: oblique }
span.classifier-delimiter {
font-family: sans-serif ;
font-weight: bold }
span.interpreted {
font-family: sans-serif }
span.option {
white-space: nowrap }
span.pre {
white-space: pre }
span.problematic, pre.problematic {
color: red }
span.section-subtitle {
/* font-size relative to parent (h1..h6 element) */
font-size: 80% }
table.citation {
border-left: solid 1px gray;
margin-left: 1px }
table.docinfo {
margin: 2em 4em }
table.docutils {
margin-top: 0.5em ;
margin-bottom: 0.5em }
table.footnote {
border-left: solid 1px black;
margin-left: 1px }
table.docutils td, table.docutils th,
table.docinfo td, table.docinfo th {
padding-left: 0.5em ;
padding-right: 0.5em ;
vertical-align: top }
table.docutils th.field-name, table.docinfo th.docinfo-name {
font-weight: bold ;
text-align: left ;
white-space: nowrap ;
padding-left: 0 }
/* "booktabs" style (no vertical lines) */
table.docutils.booktabs {
border: 0px;
border-top: 2px solid;
border-bottom: 2px solid;
border-collapse: collapse;
}
table.docutils.booktabs * {
border: 0px;
}
table.docutils.booktabs th {
border-bottom: thin solid;
text-align: left;
}
h1 tt.docutils, h2 tt.docutils, h3 tt.docutils,
h4 tt.docutils, h5 tt.docutils, h6 tt.docutils {
font-size: 100% }
ul.auto-toc {
list-style-type: none }
</style>
</head>
<body>
<div class="document" id="point-of-sale-re-order">
<h1 class="title">Point of Sale Re-order</h1>
<!-- !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! This file is generated by oca-gen-addon-readme !!
!! changes will be overwritten. !!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! source digest: sha256:4b4023ec8132da5e584b16a1c41656c5d4b8b3074ee7762ddb79902bd30b595c
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -->
<p><a class="reference external image-reference" href="https://odoo-community.org/page/development-status"><img alt="Beta" src="https://img.shields.io/badge/maturity-Beta-yellow.png" /></a> <a class="reference external image-reference" href="http://www.gnu.org/licenses/lgpl-3.0-standalone.html"><img alt="License: LGPL-3" src="https://img.shields.io/badge/licence-LGPL--3-blue.png" /></a> <a class="reference external image-reference" href="https://github.com/OCA/pos/tree/18.0/pos_order_reorder"><img alt="OCA/pos" src="https://img.shields.io/badge/github-OCA%2Fpos-lightgray.png?logo=github" /></a> <a class="reference external image-reference" href="https://translation.odoo-community.org/projects/pos-18-0/pos-18-0-pos_order_reorder"><img alt="Translate me on Weblate" src="https://img.shields.io/badge/weblate-Translate%20me-F47D42.png" /></a> <a class="reference external image-reference" href="https://runboat.odoo-community.org/builds?repo=OCA/pos&amp;target_branch=18.0"><img alt="Try me on Runboat" src="https://img.shields.io/badge/runboat-Try%20me-875A7B.png" /></a></p>
<div class="line-block">
<div class="line">This module allows you to re-order a paid order on a new order:</div>
<div class="line"><img alt="image" src="https://raw.githubusercontent.com/OCA/pos/18.0/pos_order_reorder/static/img/reorder_button.png" /></div>
</div>
<p><strong>Table of contents</strong></p>
<div class="contents local topic" id="contents">
<ul class="simple">
<li><a class="reference internal" href="#configuration" id="toc-entry-1">Configuration</a></li>
<li><a class="reference internal" href="#bug-tracker" id="toc-entry-2">Bug Tracker</a></li>
<li><a class="reference internal" href="#credits" id="toc-entry-3">Credits</a><ul>
<li><a class="reference internal" href="#authors" id="toc-entry-4">Authors</a></li>
<li><a class="reference internal" href="#contributors" id="toc-entry-5">Contributors</a></li>
<li><a class="reference internal" href="#maintainers" id="toc-entry-6">Maintainers</a></li>
</ul>
</li>
</ul>
</div>
<div class="section" id="configuration">
<h1><a class="toc-backref" href="#toc-entry-1">Configuration</a></h1>
<p>Select PoS &gt; Configuration &gt; Settings &gt; enable flag “Allow Reorder”</p>
</div>
<div class="section" id="bug-tracker">
<h1><a class="toc-backref" href="#toc-entry-2">Bug Tracker</a></h1>
<p>Bugs are tracked on <a class="reference external" href="https://github.com/OCA/pos/issues">GitHub Issues</a>.
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
<a class="reference external" href="https://github.com/OCA/pos/issues/new?body=module:%20pos_order_reorder%0Aversion:%2018.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**">feedback</a>.</p>
<p>Do not contact contributors directly about support or help with technical issues.</p>
</div>
<div class="section" id="credits">
<h1><a class="toc-backref" href="#toc-entry-3">Credits</a></h1>
<div class="section" id="authors">
<h2><a class="toc-backref" href="#toc-entry-4">Authors</a></h2>
<ul class="simple">
<li>Cetmix</li>
</ul>
</div>
<div class="section" id="contributors">
<h2><a class="toc-backref" href="#toc-entry-5">Contributors</a></h2>
<ul class="simple">
<li>Cetmix &lt;<a class="reference external" href="https://cetmix.com/">https://cetmix.com/</a>&gt;</li>
<li>Dinar Gabbasov</li>
<li><a class="reference external" href="https://www.heliconia.io">Heliconia Solutions Pvt. Ltd.</a><ul>
<li>Bhavesh Heliconia</li>
</ul>
</li>
</ul>
</div>
<div class="section" id="maintainers">
<h2><a class="toc-backref" href="#toc-entry-6">Maintainers</a></h2>
<p>This module is maintained by the OCA.</p>
<a class="reference external image-reference" href="https://odoo-community.org">
<img alt="Odoo Community Association" src="https://odoo-community.org/logo.png" />
</a>
<p>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.</p>
<p>This module is part of the <a class="reference external" href="https://github.com/OCA/pos/tree/18.0/pos_order_reorder">OCA/pos</a> project on GitHub.</p>
<p>You are welcome to contribute. To learn how please visit <a class="reference external" href="https://odoo-community.org/page/Contribute">https://odoo-community.org/page/Contribute</a>.</p>
</div>
</div>
</div>
</body>
</html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 152 KiB

View file

@ -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();
}
}

View file

@ -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 },
});

View file

@ -0,0 +1,15 @@
<?xml version="1.0" encoding="UTF-8" ?>
<templates id="template" xml:space="preserve">
<t t-name="ReorderButton">
<span
class="control-button btn btn-light btn-lg lh-lg flex-grow-1 flex-shrink-1"
t-on-click="_onClick"
>
<i class="fa fa-copy" />
<span />
<span>Re-order</span>
</span>
</t>
</templates>

View file

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8" ?>
<templates id="template" xml:space="preserve">
<t
t-name="TicketScreen"
t-inherit="point_of_sale.TicketScreen"
t-inherit-mode="extension"
>
<xpath expr="//InvoiceButton" position="after">
<ReorderButton order="_selectedSyncedOrder" />
</xpath>
</t>
</templates>

View file

@ -0,0 +1,29 @@
<?xml version="1.0" encoding="utf-8" ?>
<odoo>
<record id="res_config_settings_view_form" model="ir.ui.view">
<field name="name">res.config.settings.view.form</field>
<field name="model">res.config.settings</field>
<field
name="inherit_id"
ref="point_of_sale.res_config_settings_view_form"
/>
<field name="arch" type="xml">
<xpath
expr="//block[@id='pos_interface_section']"
position="inside"
>
<div class="col-12 col-lg-6 o_setting_box">
<div class="o_setting_left_pane">
<field name="pos_allow_reorder" />
</div>
<div class="o_setting_right_pane">
<label for="pos_allow_reorder" />
<div class="text-muted">
Creating a new POS order based on existing one
</div>
</div>
</div>
</xpath>
</field>
</record>
</odoo>

View file

@ -0,0 +1 @@
from . import models

View file

@ -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",
],
}

View file

@ -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

View file

@ -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 "",
),
)

View file

@ -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"

View file

@ -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,
)

View file

@ -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,
)

View file

@ -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,
)

View file

@ -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).

View file

@ -0,0 +1 @@
from . import test_sort_lines_by_product_name

View file

@ -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}])

View file

@ -0,0 +1 @@
from . import models

View file

@ -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",
],
}

View file

@ -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

View file

@ -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)

View file

@ -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",
)

View file

@ -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)

View file

@ -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

View file

@ -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

View file

@ -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).

View file

@ -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.

View file

@ -0,0 +1 @@
from . import test_stock_picking_report_undelivered_product

View file

@ -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]))

View file

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8" ?>
<odoo>
<record id="view_template_property_form" model="ir.ui.view">
<field
name="name"
>product.template.form.inherit.undelivered.product</field>
<field name="model">product.template</field>
<field name="inherit_id" ref="stock.view_template_property_form" />
<field name="arch" type="xml">
<group name="group_lots_and_weight" position="inside">
<field name="display_undelivered_in_picking" />
</group>
</field>
</record>
</odoo>

View file

@ -0,0 +1,92 @@
<?xml version="1.0" encoding="utf-8" ?>
<odoo>
<template id="undeliverd_product">
<t
t-if="o.partner_id.display_undelivered_in_picking and o.state == 'done'"
>
<!-- Get stock moves from backorder according to setting values -->
<t
t-if="not o.company_id.undelivered_product_slip_report_method or o.company_id.undelivered_product_slip_report_method == 'all'"
>
<t
t-set="undelivered_moves"
t-value="o.mapped('move_ids').filtered(lambda l: l.quantity == 0.0 and l.state == 'cancel' and l.product_id.display_undelivered_in_picking)"
/>
</t>
<t
t-if="o.company_id.undelivered_product_slip_report_method == 'partially_undelivered'"
>
<t
t-set="undelivered_moves"
t-value="o.mapped('move_ids').filtered(lambda l: l.state == 'cancel' and l.product_id.display_undelivered_in_picking and l.splitted_stock_move_orig_id)"
/>
</t>
<t
t-if="o.company_id.undelivered_product_slip_report_method == 'completely_undelivered'"
>
<t
t-set="undelivered_moves"
t-value="o.mapped('move_ids').filtered(lambda l: l.state == 'cancel' and l.product_id.display_undelivered_in_picking and not l.splitted_stock_move_orig_id)"
/>
</t>
<table
t-if="undelivered_moves"
class="table table-sm table-bordered mt0"
id="undelivered_product"
>
<thead>
<tr><th colspan="7"><strong
>Remaining products (not delivered yet)</strong></th></tr>
<tr>
<th><strong>Reference</strong></th>
<th><strong>Product</strong></th>
<th class="text-end"><strong>Quantity</strong></th>
</tr>
</thead>
<tbody>
<t
t-foreach="undelivered_moves.sorted(key=lambda l: (l.product_id.default_code or '', l.product_id.name))"
t-as="undelivered_move"
>
<tr
t-att-class="'partially_undelivered_line' if undelivered_move.splitted_stock_move_orig_id else 'completely_undelivered_line'"
>
<td>
<span
t-field="undelivered_move.product_id.default_code"
/>
</td>
<td>
<span
t-field="undelivered_move.product_id.name"
/>
</td>
<td class="text-end">
<span
t-field="undelivered_move.product_uom_qty"
/> <span
groups="uom.group_uom"
t-field="undelivered_move.product_uom"
/>
</td>
</tr>
</t>
</tbody>
</table>
</t>
</template>
<template
id="report_delivery_document"
inherit_id="stock.report_delivery_document"
priority="200"
>
<xpath expr="//table[@name='stock_move_line_table']" position="after">
<t
t-call="stock_picking_report_undelivered_product.undeliverd_product"
/>
</xpath>
</template>
</odoo>

View file

@ -0,0 +1,25 @@
<?xml version="1.0" encoding="utf-8" ?>
<odoo>
<record id="res_config_settings_view_form" model="ir.ui.view">
<field
name="name"
>res.config.settings.view.form.inherit.undelivered.product</field>
<field name="model">res.config.settings</field>
<field name="inherit_id" ref="stock.res_config_settings_view_form" />
<field name="arch" type="xml">
<block name="operations_setting_container" position="inside">
<setting
id="undelivered_product_slip_report_method"
string="Undelivered products"
help="Method to display undelivered product lines in the delivery slip report"
>
<field
name="undelivered_product_slip_report_method"
class="o_light_label"
widget="selection"
/>
</setting>
</block>
</field>
</record>
</odoo>

View file

@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8" ?>
<odoo>
<record id="view_partner_stock_form" model="ir.ui.view">
<field name="name">res.partner.form.inherit.undelivered.product</field>
<field name="model">res.partner</field>
<field name="inherit_id" ref="stock.view_partner_stock_form" />
<field name="arch" type="xml">
<group name="inventory" position="inside">
<field name="display_undelivered_in_picking" />
</group>
</field>
</record>
</odoo>