Compare commits

...

2 commits

21 changed files with 929 additions and 0 deletions

View file

@ -0,0 +1,61 @@
# Account Invoice Triple Discount Readonly
## Description
This module fixes a bug in `account_invoice_triple_discount` when combined with `purchase_triple_discount`.
### Original Problem
In purchase orders, when configuring a discount in the `discount2` or `discount3` columns, upon saving:
- All discounts were accumulated in the `discount1` column
- The `discount2` and `discount3` fields were reset to zero
This occurred because the original mixin's `write` method always reset `discount2` and `discount3` to 0 when the computed `discount` field was included in the write (which happens when any discount is modified).
### Solution
The module overrides the `write` method of `triple.discount.mixin` to:
1. Detect if `discount1`, `discount2` or `discount3` are being explicitly modified
2. If there are explicit discounts, ignore the computed `discount` field
3. If only the `discount` field comes, maintain legacy behavior (map to `discount1` and reset the rest)
Additionally, it makes the `discount` field (total discount) **readonly** in:
- Partner form view (`res.partner`)
- Supplier info in product form (`product.supplierinfo`)
- Base mixin (`triple.discount.mixin`)
This forces users to work only with the `discount1`, `discount2` and `discount3` columns, avoiding confusion.
## Installation
1. Make sure `account_invoice_triple_discount` and `purchase_triple_discount` are installed
2. Update the module list
3. Install `account_invoice_triple_discount_readonly`
## Usage
After installation:
- The total discount field will be readonly and automatically calculated
- Users must use discount1, discount2, and discount3 fields to set discounts
- When saving, the individual discount values will be preserved correctly
## Bug Tracker
Bugs are tracked on [GitHub Issues](https://github.com/OCA/account-invoicing/issues).
In case of trouble, please check there if your issue has already been reported.
## Credits
### Contributors
* Coop Makers
### Maintainers
This module is maintained by the OCA.
## Compatibility
- Odoo 16.0
- account_invoice_triple_discount
- purchase_triple_discount

View file

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

View file

@ -0,0 +1,18 @@
# Copyright 2025
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
{
"name": "Account Invoice Triple Discount Readonly",
"version": "16.0.1.0.0",
"summary": "Make total discount readonly and fix discount2/discount3 write issue",
"license": "AGPL-3",
"author": "Criptomart",
"website": "https://github.com/OCA/account-invoicing",
"depends": ["account_invoice_triple_discount", "purchase_triple_discount"],
"data": [
"views/product_supplierinfo_view.xml",
"views/res_partner_view.xml",
"views/purchase_order_view.xml",
"views/account_move_view.xml",
],
"installable": True,
}

View file

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

View file

@ -0,0 +1,28 @@
from odoo import fields, models
class TripleDiscountMixin(models.AbstractModel):
_inherit = "triple.discount.mixin"
# Make the discount field readonly to prevent manual edits
discount = fields.Float(readonly=True)
def write(self, vals):
discount_fields = self._get_multiple_discount_field_names()
if "discount" in vals:
# Check if any of discount1, discount2 or discount3
# are being explicitly modified
has_explicit_discounts = any(field in vals for field in discount_fields)
if has_explicit_discounts:
# If there are explicit discounts, remove the computed 'discount' field
# to avoid overwriting the explicit values
vals.pop("discount")
else:
# If only 'discount' comes, apply legacy behavior:
# map to discount1 and reset discount2 and discount3
vals["discount1"] = vals.pop("discount")
vals.update({field: 0 for field in discount_fields[1:]})
return super().write(vals)

View file

@ -0,0 +1,28 @@
<?xml version="1.0" encoding="utf-8" ?>
<odoo>
<!-- Make discount field readonly in invoice lines -->
<record id="invoice_triple_discount_readonly" model="ir.ui.view">
<field name="name">account.invoice.triple.discount.readonly</field>
<field name="model">account.move</field>
<field
name="inherit_id"
ref="account_invoice_triple_discount.invoice_triple_discount_form_view"
/>
<field name="arch" type="xml">
<xpath
expr="//field[@name='invoice_line_ids']//tree//field[@name='discount']"
position="attributes"
>
<attribute name="readonly">1</attribute>
</xpath>
<xpath
expr="//field[@name='invoice_line_ids']//form//field[@name='discount']"
position="attributes"
>
<attribute name="readonly">1</attribute>
</xpath>
</field>
</record>
</odoo>

View file

@ -0,0 +1,32 @@
<?xml version="1.0" encoding="utf-8" ?>
<odoo>
<!-- Make discount field readonly in supplier info form view -->
<record model="ir.ui.view" id="product_supplierinfo_form_view_readonly_discount">
<field name="model">product.supplierinfo</field>
<field
name="inherit_id"
ref="purchase_triple_discount.product_supplierinfo_form_view"
/>
<field name="arch" type="xml">
<xpath expr="//field[@name='discount']" position="attributes">
<attribute name="invisible">1</attribute>
</xpath>
</field>
</record>
<!-- Make discount field readonly in supplier info tree view -->
<record model="ir.ui.view" id="product_supplierinfo_tree_view_readonly_discount">
<field name="model">product.supplierinfo</field>
<field
name="inherit_id"
ref="purchase_triple_discount.product_supplierinfo_tree_view"
/>
<field name="arch" type="xml">
<xpath expr="//field[@name='discount']" position="attributes">
<attribute name="invisible">1</attribute>
</xpath>
</field>
</record>
</odoo>

View file

@ -0,0 +1,28 @@
<?xml version="1.0" encoding="utf-8" ?>
<odoo>
<!-- Make discount field readonly in purchase order lines -->
<record id="purchase_order_triple_discount_readonly" model="ir.ui.view">
<field name="name">purchase.order.triple.discount.readonly</field>
<field name="model">purchase.order</field>
<field
name="inherit_id"
ref="purchase_triple_discount.purchase_order_triple_discount_form_view"
/>
<field name="arch" type="xml">
<xpath
expr="//field[@name='order_line']//tree//field[@name='discount']"
position="attributes"
>
<attribute name="readonly">1</attribute>
</xpath>
<xpath
expr="//field[@name='order_line']//form//field[@name='discount']"
position="attributes"
>
<attribute name="readonly">1</attribute>
</xpath>
</field>
</record>
</odoo>

View file

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8" ?>
<odoo>
<!-- Make default_supplierinfo_discount readonly in partner form view -->
<record model="ir.ui.view" id="res_partner_form_view_readonly_discount">
<field name="model">res.partner</field>
<field
name="inherit_id"
ref="purchase_triple_discount.res_partner_form_view"
/>
<field name="arch" type="xml">
<field name="default_supplierinfo_discount" position="attributes">
<attribute name="invisible">1</attribute>
</field>
</field>
</record>
</odoo>

View file

@ -0,0 +1,95 @@
===========================
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/16.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-16-0/pos-16-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=16.0
:alt: Try me on Runboat
|badge1| |badge2| |badge3| |badge4| |badge5|
This module adds a new button in the ticket screen of refund process to
automatically add every line of the selected order and returns to the
main screen.
.. 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:%2016.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/16.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

View file

@ -0,0 +1,22 @@
{
"name": "Point of Sale - Full Refund",
"summary": "Add button to easily perform full refunds in Point of Sale",
"author": "Innovyou, Odoo Community Association (OCA)",
"website": "https://github.com/OCA/pos",
"development_status": "Alpha",
"category": "Point of sale",
"maintainers": ["LorenzoC0"],
"version": "16.0.1.0.0",
"license": "AGPL-3",
"installable": True,
"depends": ["point_of_sale"],
"assets": {
"point_of_sale.assets": [
"pos_full_refund/static/src/js/TicketScreen.esm.js",
"pos_full_refund/static/src/xml/pos_full_refund.xml",
],
"web.assets_tests": [
"pos_full_refund/static/tests/tours/PosFullRefund.tour.esm.js",
],
},
}

View file

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

View file

@ -0,0 +1,4 @@
- [Innovyou] (https://www.innovyou.it):
- Lorenzo Carta
- Lorenzo Battistini
- Valerio Paretta

View file

@ -0,0 +1 @@
This module adds a new button in the ticket screen of refund process to automatically add every line of the selected order and returns to the main screen.

View file

@ -0,0 +1,438 @@
<!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-full-refund">
<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/16.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-16-0/pos-16-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=16.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 new button in the ticket screen of refund process to
automatically add every line of the selected order and returns to the
main screen.</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:%2016.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/16.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,27 @@
/** @odoo-module **/
import TicketScreen from "point_of_sale.TicketScreen";
import {patch} from "@web/core/utils/patch";
patch(TicketScreen.prototype, "pos_full_refund.TicketScreen", {
onDoFullRefund() {
const order = this.getSelectedSyncedOrder();
if (!order) {
return;
}
// Set all orderlines to be fully refunded
// In Odoo 16, we use env.pos.toRefundLines instead of order.uiState.lineToRefund
for (const line of order.get_orderlines()) {
const refundableQty = line.get_quantity() - line.refunded_qty;
if (refundableQty > 0) {
// Get or create refund detail for this orderline
const toRefundDetail = this._getToRefundDetail(line);
toRefundDetail.qty = refundableQty;
}
}
// Trigger the refund process
this._onDoRefund();
},
});

View file

@ -0,0 +1,26 @@
<?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"
owl="1"
>
<xpath
expr="//div[hasclass('leftpane')]//div[hasclass('control-buttons')]"
position="inside"
>
<button
id="set_full_refund_button"
class='control-button btn btn-light rounded-0 fw-bolder'
t-if="_selectedSyncedOrder"
t-on-click="() => this.onDoFullRefund()"
>
<i class="fa fa-cart-arrow-down" />
Do Full Refund
</button>
</xpath>
</t>
</templates>

View file

@ -0,0 +1,62 @@
odoo.define("pos_full_refund.tour.PosFullRefundTour", function (require) {
const {ProductScreen} = require("point_of_sale.tour.ProductScreenTourMethods");
const {PaymentScreen} = require("point_of_sale.tour.PaymentScreenTourMethods");
const {ReceiptScreen} = require("point_of_sale.tour.ReceiptScreenTourMethods");
const {TicketScreen} = require("point_of_sale.tour.TicketScreenTourMethods");
const {getSteps, startSteps} = require("point_of_sale.tour.utils");
var Tour = require("web_tour.tour");
startSteps();
// Start POS and open session
ProductScreen.do.confirmOpeningPopup();
// Create an order with multiple products
ProductScreen.exec.addOrderline("Desk Pad", "2", "5");
ProductScreen.exec.addOrderline("Monitor Stand", "3", "4.5");
ProductScreen.exec.addOrderline("Letter Tray", "1", "5");
// Verify the order has multiple lines by checking the selected orderline
ProductScreen.check.selectedOrderlineHas("Letter Tray", "1.0");
// Pay for the order
ProductScreen.do.clickPayButton();
PaymentScreen.do.clickPaymentMethod("Bank");
PaymentScreen.do.clickValidate();
ReceiptScreen.check.isShown();
ReceiptScreen.do.clickNextOrder();
// Go to refund mode
ProductScreen.do.clickRefund();
// Filter should be automatically 'Paid'
TicketScreen.check.filterIs("Paid");
// Select the order we just created
TicketScreen.do.selectOrder("-0001");
// Click the "Do Full Refund" button
getSteps().push({
trigger: "#set_full_refund_button",
run: "click",
});
// Verify we're back on product screen with refund order
ProductScreen.check.isShown();
// Verify all lines are in the refund order with negative quantities
// We can check by selecting each line and verifying the quantity
ProductScreen.check.selectedOrderlineHas("Letter Tray", "-1.0");
ProductScreen.do.clickOrderline("Desk Pad", "-2.0");
ProductScreen.check.selectedOrderlineHas("Desk Pad", "-2.0");
ProductScreen.do.clickOrderline("Monitor Stand", "-3.0");
ProductScreen.check.selectedOrderlineHas("Monitor Stand", "-3.0");
// Complete the refund by paying
ProductScreen.do.clickPayButton();
PaymentScreen.do.clickPaymentMethod("Bank");
PaymentScreen.do.clickValidate();
ReceiptScreen.check.isShown();
Tour.register("PosFullRefundTour", {test: true, url: "/pos/ui"}, getSteps());
});

View file

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

View file

@ -0,0 +1,35 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo.tests import tagged
from odoo.addons.point_of_sale.tests.test_frontend import TestPointOfSaleHttpCommon
@tagged("post_install", "-at_install")
class TestPosFullRefund(TestPointOfSaleHttpCommon):
"""Test the Full Refund functionality in POS.
This test verifies that when a user clicks the "Do Full Refund" button
in the ticket screen, all lines from the selected order are automatically
added to a new refund order with their full quantities.
"""
def test_pos_full_refund(self):
"""Test that clicking 'Do Full Refund' creates a refund order with all lines."""
# Open a POS session
self.main_pos_config.open_ui()
# Start the tour that will:
# 1. Create an order with multiple products
# (Desk Pad, Monitor Stand, Letter Tray)
# 2. Pay for the order
# 3. Open refund mode
# 4. Select the paid order
# 5. Click "Do Full Refund" button
# 6. Verify all lines are in the refund order with negative quantities
# 7. Complete the refund payment
self.start_tour(
"/pos/ui?config_id=%d" % self.main_pos_config.id,
"PosFullRefundTour",
login="accountman",
)