Merge PR #3080 into 17.0

Signed-off-by pedrobaeza
pull/3126/head
OCA-git-bot 2025-03-17 17:37:27 +00:00
commit 6e5095a135
19 changed files with 1431 additions and 0 deletions

View File

@ -0,0 +1,112 @@
=========================================
Web widget product label section and note
=========================================
..
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! This file is generated by oca-gen-addon-readme !!
!! changes will be overwritten. !!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! source digest: sha256:db9a30e28410d3ef4ad626f850f766eedb3845976f424d1bbae3350674397d0d
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png
:target: https://odoo-community.org/page/development-status
:alt: Beta
.. |badge2| image:: https://img.shields.io/badge/licence-AGPL--3-blue.png
:target: http://www.gnu.org/licenses/agpl-3.0-standalone.html
:alt: License: AGPL-3
.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fweb-lightgray.png?logo=github
:target: https://github.com/OCA/web/tree/17.0/web_widget_product_label_section_and_note
:alt: OCA/web
.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png
:target: https://translation.odoo-community.org/projects/web-17-0/web-17-0-web_widget_product_label_section_and_note
: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/web&target_branch=17.0
:alt: Try me on Runboat
|badge1| |badge2| |badge3| |badge4| |badge5|
This module unifies the product and name into a single column, making it
more user-friendly and space-saving.
**Table of contents**
.. contents::
:local:
Configuration
=============
To extend this functionality to other modules (for example, Purchase),
in a new module, it is necessary to create an inherited view to change
the widget for these fields:
- one2many field with widget="product_label_section_and_note_field_o2m".
- product_id or producttmpl_id field with
widget="product_label_section_and_note_field".
- name field with widget="section_and_note_text".
Usage
=====
- Go to the **Invoicing > Customers > Invoices** and add a new line.
- The product label must be displayed just after the product name.
- If the product does not have a description, a button must be displayed
to add one.
Known issues / Roadmap
======================
- Add compatibility with sale_product_configurator module
- Add compatibility with purchase_product_matrix module
- When this module is installed, the PDF report will always display the
column name as a combination of the product code, product name, and
description. This behavior may differ from the expected behavior,
where only the column name is displayed and can be customized
independently.
Bug Tracker
===========
Bugs are tracked on `GitHub Issues <https://github.com/OCA/web/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/web/issues/new?body=module:%20web_widget_product_label_section_and_note%0Aversion:%2017.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
-------
* Tecnativa
* Odoo S.A.
Contributors
------------
- `Tecnativa <https://www.tecnativa.com>`__:
- Pedro M. Baeza
- Carlos Lopez
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/web <https://github.com/OCA/web/tree/17.0/web_widget_product_label_section_and_note>`_ project on GitHub.
You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

View File

@ -0,0 +1,23 @@
{
"name": "Web widget product label section and note",
"version": "17.0.1.0.0",
"summary": "unify the product and name into a single column",
"author": "Tecnativa, Odoo Community Association (OCA), Odoo S.A.",
"website": "https://github.com/OCA/web",
"depends": [
"web",
"account",
],
"data": [
"views/account_move_views.xml",
],
"assets": {
"web.assets_backend": [
"web_widget_product_label_section_and_note/static/src/core/utils/**/*",
"web_widget_product_label_section_and_note/static/src/components/**/*",
],
},
"installable": True,
"auto_install": False,
"license": "AGPL-3",
}

View File

@ -0,0 +1,67 @@
# Translation of Odoo Server.
# This file contains the translation of the following modules:
# * web_widget_product_label_section_and_note
#
msgid ""
msgstr ""
"Project-Id-Version: Odoo Server 16.0\n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2024-12-14 11:06+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 5.6.2\n"
#. module: web_widget_product_label_section_and_note
#. odoo-javascript
#: code:addons/web_widget_product_label_section_and_note/static/src/components/product_label_section_and_note_field/product_label_section_and_note_field.xml:0
#, python-format
msgid "Click or press enter to add a description"
msgstr "Fare clic o premere invio per aggiungere una descrizione"
#. module: web_widget_product_label_section_and_note
#. odoo-javascript
#: code:addons/web_widget_product_label_section_and_note/static/src/components/product_label_section_and_note_field/product_label_section_and_note_field.xml:0
#, python-format
msgid "Click to remove/add the product name from the description."
msgstr ""
"Fare clic per rimuovere/aggiungere il nome del prodotto dalla descrizione."
#. module: web_widget_product_label_section_and_note
#. odoo-javascript
#: code:addons/web_widget_product_label_section_and_note/static/src/components/product_label_section_and_note_field/product_label_section_and_note_field.xml:0
#: code:addons/web_widget_product_label_section_and_note/static/src/components/product_label_section_and_note_field/product_label_section_and_note_field.xml:0
#: code:addons/web_widget_product_label_section_and_note/static/src/components/product_label_section_and_note_field/product_label_section_and_note_field.xml:0
#: code:addons/web_widget_product_label_section_and_note/static/src/components/product_label_section_and_note_field/product_label_section_and_note_field.xml:0
#, python-format
msgid "Enter a description"
msgstr "Inserire una descrizione"
#. module: web_widget_product_label_section_and_note
#. odoo-javascript
#: code:addons/web_widget_product_label_section_and_note/static/src/components/product_label_section_and_note_field/product_label_section_and_note_field.xml:0
#: code:addons/web_widget_product_label_section_and_note/static/src/components/product_label_section_and_note_field/product_label_section_and_note_field.xml:0
#, python-format
msgid "Internal link"
msgstr "Link interno"
#. module: web_widget_product_label_section_and_note
#. odoo-javascript
#: code:addons/web_widget_product_label_section_and_note/static/src/components/product_label_section_and_note_field/product_label_section_and_note_field.xml:0
#: code:addons/web_widget_product_label_section_and_note/static/src/components/product_label_section_and_note_field/product_label_section_and_note_field.xml:0
#: code:addons/web_widget_product_label_section_and_note/static/src/components/product_label_section_and_note_field/product_label_section_and_note_field.xml:0
#, python-format
msgid "Scan barcode"
msgstr "Leggi codice a barre"
#. module: web_widget_product_label_section_and_note
#. odoo-javascript
#: code:addons/web_widget_product_label_section_and_note/static/src/components/product_label_section_and_note_field/product_label_section_and_note_field.esm.js:0
#: code:addons/web_widget_product_label_section_and_note/static/src/components/product_label_section_and_note_field/product_label_section_and_note_field.xml:0
#, python-format
msgid "Search a product"
msgstr "Cerca un prodotto"

View File

@ -0,0 +1,63 @@
# Translation of Odoo Server.
# This file contains the translation of the following modules:
# * web_widget_product_label_section_and_note
#
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: web_widget_product_label_section_and_note
#. odoo-javascript
#: code:addons/web_widget_product_label_section_and_note/static/src/components/product_label_section_and_note_field/product_label_section_and_note_field.xml:0
#, python-format
msgid "Click or press enter to add a description"
msgstr ""
#. module: web_widget_product_label_section_and_note
#. odoo-javascript
#: code:addons/web_widget_product_label_section_and_note/static/src/components/product_label_section_and_note_field/product_label_section_and_note_field.xml:0
#, python-format
msgid "Click to remove/add the product name from the description."
msgstr ""
#. module: web_widget_product_label_section_and_note
#. odoo-javascript
#: code:addons/web_widget_product_label_section_and_note/static/src/components/product_label_section_and_note_field/product_label_section_and_note_field.xml:0
#: code:addons/web_widget_product_label_section_and_note/static/src/components/product_label_section_and_note_field/product_label_section_and_note_field.xml:0
#: code:addons/web_widget_product_label_section_and_note/static/src/components/product_label_section_and_note_field/product_label_section_and_note_field.xml:0
#: code:addons/web_widget_product_label_section_and_note/static/src/components/product_label_section_and_note_field/product_label_section_and_note_field.xml:0
#, python-format
msgid "Enter a description"
msgstr ""
#. module: web_widget_product_label_section_and_note
#. odoo-javascript
#: code:addons/web_widget_product_label_section_and_note/static/src/components/product_label_section_and_note_field/product_label_section_and_note_field.xml:0
#: code:addons/web_widget_product_label_section_and_note/static/src/components/product_label_section_and_note_field/product_label_section_and_note_field.xml:0
#, python-format
msgid "Internal link"
msgstr ""
#. module: web_widget_product_label_section_and_note
#. odoo-javascript
#: code:addons/web_widget_product_label_section_and_note/static/src/components/product_label_section_and_note_field/product_label_section_and_note_field.xml:0
#: code:addons/web_widget_product_label_section_and_note/static/src/components/product_label_section_and_note_field/product_label_section_and_note_field.xml:0
#: code:addons/web_widget_product_label_section_and_note/static/src/components/product_label_section_and_note_field/product_label_section_and_note_field.xml:0
#, python-format
msgid "Scan barcode"
msgstr ""
#. module: web_widget_product_label_section_and_note
#. odoo-javascript
#: code:addons/web_widget_product_label_section_and_note/static/src/components/product_label_section_and_note_field/product_label_section_and_note_field.esm.js:0
#: code:addons/web_widget_product_label_section_and_note/static/src/components/product_label_section_and_note_field/product_label_section_and_note_field.xml:0
#, python-format
msgid "Search a product"
msgstr ""

View File

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

View File

@ -0,0 +1,8 @@
To extend this functionality to other modules (for example, Purchase),
in a new module, it is necessary to create an inherited view to change
the widget for these fields:
- one2many field with widget="product_label_section_and_note_field_o2m".
- product_id or producttmpl_id field with
widget="product_label_section_and_note_field".
- name field with widget="section_and_note_text".

View File

@ -0,0 +1,4 @@
- [Tecnativa](https://www.tecnativa.com):
> - Pedro M. Baeza
> - Carlos Lopez

View File

@ -0,0 +1,2 @@
This module unifies the product and name into a single column, making it
more user-friendly and space-saving.

View File

@ -0,0 +1,7 @@
- Add compatibility with sale_product_configurator module
- Add compatibility with purchase_product_matrix module
- When this module is installed, the PDF report will always display the
column name as a combination of the product code, product name, and
description. This behavior may differ from the expected behavior,
where only the column name is displayed and can be customized
independently.

View File

@ -0,0 +1,4 @@
- Go to the **Invoicing \> Customers \> Invoices** and add a new line.
- The product label must be displayed just after the product name.
- If the product does not have a description, a button must be displayed
to add one.

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.2 KiB

View File

@ -0,0 +1,468 @@
<!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>Web widget product label section and note</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="web-widget-product-label-section-and-note">
<h1 class="title">Web widget product label section and note</h1>
<!-- !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! This file is generated by oca-gen-addon-readme !!
!! changes will be overwritten. !!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! source digest: sha256:db9a30e28410d3ef4ad626f850f766eedb3845976f424d1bbae3350674397d0d
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -->
<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/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/web/tree/17.0/web_widget_product_label_section_and_note"><img alt="OCA/web" src="https://img.shields.io/badge/github-OCA%2Fweb-lightgray.png?logo=github" /></a> <a class="reference external image-reference" href="https://translation.odoo-community.org/projects/web-17-0/web-17-0-web_widget_product_label_section_and_note"><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/web&amp;target_branch=17.0"><img alt="Try me on Runboat" src="https://img.shields.io/badge/runboat-Try%20me-875A7B.png" /></a></p>
<p>This module unifies the product and name into a single column, making it
more user-friendly and space-saving.</p>
<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="#usage" id="toc-entry-2">Usage</a></li>
<li><a class="reference internal" href="#known-issues-roadmap" id="toc-entry-3">Known issues / Roadmap</a></li>
<li><a class="reference internal" href="#bug-tracker" id="toc-entry-4">Bug Tracker</a></li>
<li><a class="reference internal" href="#credits" id="toc-entry-5">Credits</a><ul>
<li><a class="reference internal" href="#authors" id="toc-entry-6">Authors</a></li>
<li><a class="reference internal" href="#contributors" id="toc-entry-7">Contributors</a></li>
<li><a class="reference internal" href="#maintainers" id="toc-entry-8">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>To extend this functionality to other modules (for example, Purchase),
in a new module, it is necessary to create an inherited view to change
the widget for these fields:</p>
<ul class="simple">
<li>one2many field with widget=”product_label_section_and_note_field_o2m”.</li>
<li>product_id or producttmpl_id field with
widget=”product_label_section_and_note_field”.</li>
<li>name field with widget=”section_and_note_text”.</li>
</ul>
</div>
<div class="section" id="usage">
<h1><a class="toc-backref" href="#toc-entry-2">Usage</a></h1>
<ul class="simple">
<li>Go to the <strong>Invoicing &gt; Customers &gt; Invoices</strong> and add a new line.</li>
<li>The product label must be displayed just after the product name.</li>
<li>If the product does not have a description, a button must be displayed
to add one.</li>
</ul>
</div>
<div class="section" id="known-issues-roadmap">
<h1><a class="toc-backref" href="#toc-entry-3">Known issues / Roadmap</a></h1>
<ul class="simple">
<li>Add compatibility with sale_product_configurator module</li>
<li>Add compatibility with purchase_product_matrix module</li>
<li>When this module is installed, the PDF report will always display the
column name as a combination of the product code, product name, and
description. This behavior may differ from the expected behavior,
where only the column name is displayed and can be customized
independently.</li>
</ul>
</div>
<div class="section" id="bug-tracker">
<h1><a class="toc-backref" href="#toc-entry-4">Bug Tracker</a></h1>
<p>Bugs are tracked on <a class="reference external" href="https://github.com/OCA/web/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/web/issues/new?body=module:%20web_widget_product_label_section_and_note%0Aversion:%2017.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-5">Credits</a></h1>
<div class="section" id="authors">
<h2><a class="toc-backref" href="#toc-entry-6">Authors</a></h2>
<ul class="simple">
<li>Tecnativa</li>
<li>Odoo S.A.</li>
</ul>
</div>
<div class="section" id="contributors">
<h2><a class="toc-backref" href="#toc-entry-7">Contributors</a></h2>
<ul>
<li><p class="first"><a class="reference external" href="https://www.tecnativa.com">Tecnativa</a>:</p>
<blockquote>
<ul class="simple">
<li>Pedro M. Baeza</li>
<li>Carlos Lopez</li>
</ul>
</blockquote>
</li>
</ul>
</div>
<div class="section" id="maintainers">
<h2><a class="toc-backref" href="#toc-entry-8">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/web/tree/17.0/web_widget_product_label_section_and_note">OCA/web</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,300 @@
/** @odoo-module **/
/* Copyright Odoo S.A.
* Copyright 2024 Tecnativa - Carlos Lopez
* License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). */
import {
onMounted,
onPatched,
onWillUnmount,
useEffect,
useRef,
useState,
} from "@odoo/owl";
import {AutoComplete} from "@web/core/autocomplete/autocomplete";
import {Many2OneField} from "@web/views/fields/many2one/many2one_field";
import {Many2XAutocomplete} from "@web/views/fields/relational_utils";
import {
SectionAndNoteListRenderer,
sectionAndNoteFieldOne2Many,
} from "@account/components/section_and_note_fields_backend/section_and_note_fields_backend";
import {X2ManyField, x2ManyField} from "@web/views/fields/x2many/x2many_field";
import {_t} from "@web/core/l10n/translation";
import {useRecordObserver} from "@web/model/relational_model/utils";
import {getActiveHotkey} from "@web/core/hotkeys/hotkey_service";
import {registry} from "@web/core/registry";
import {useProductAndLabelAutoresize} from "../../core/utils/product_and_label_autoresize.esm";
export class ProductLabelSectionAndNoteListRender extends SectionAndNoteListRenderer {
setup() {
this.productColumns = ["product_id", "product_template_id"];
// We need to store the titleField in a temporary variable
// because the titleField is set in the parent setup method
// and the function getActiveColumns is called before the setup method in the account module
this.titleField_tmp = "";
super.setup();
if (this.titleField_tmp) {
this.titleField = this.titleField_tmp;
}
}
getCellTitle(column, record) {
// When using this list renderer, we don't want the product_id cell to have a tooltip with its label.
if (this.productColumns.includes(column.name)) {
return;
}
super.getCellTitle(column, record);
}
getActiveColumns(list) {
let activeColumns = super.getActiveColumns(list);
const productCol = activeColumns.find((col) =>
this.productColumns.includes(col.name)
);
const labelCol = activeColumns.find((col) => col.name === "name");
if (productCol) {
if (labelCol) {
list.records.forEach(
(record) => (record.columnIsProductAndLabel = true)
);
} else {
list.records.forEach(
(record) => (record.columnIsProductAndLabel = false)
);
}
activeColumns = activeColumns.filter((col) => col.name !== "name");
this.titleField_tmp = productCol.name;
} else {
this.titleField_tmp = "name";
}
return activeColumns;
}
}
export class ProductLabelSectionAndNoteOne2Many extends X2ManyField {
static components = {
...X2ManyField.components,
ListRenderer: ProductLabelSectionAndNoteListRender,
};
}
export const productLabelSectionAndNoteOne2Many = {
...x2ManyField,
component: ProductLabelSectionAndNoteOne2Many,
additionalClasses: sectionAndNoteFieldOne2Many.additionalClasses,
};
registry
.category("fields")
.add(
"product_label_section_and_note_field_o2m",
productLabelSectionAndNoteOne2Many
);
export class ProductLabelSectionAndNoteAutocomplete extends AutoComplete {
setup() {
super.setup();
this.labelTextarea = useRef("labelNodeRef");
}
onInputKeydown(event) {
super.onInputKeydown(event);
const hotkey = getActiveHotkey(event);
const labelVisibilityButton = document.getElementById(
"labelVisibilityButtonId"
);
if (hotkey === "enter") {
if (labelVisibilityButton && !this.labelTextarea.el) {
labelVisibilityButton.click();
event.stopPropagation();
event.preventDefault();
}
}
}
}
export class ProductLabelSectionAndNoteFieldAutocomplete extends Many2XAutocomplete {
static components = {
...Many2XAutocomplete.components,
AutoComplete: ProductLabelSectionAndNoteAutocomplete,
};
static props = {
...Many2XAutocomplete.props,
isNote: {type: Boolean},
isSection: {type: Boolean},
onFocusout: {type: Function, optional: true},
updateLabel: {type: Function, optional: true},
};
static template =
"web_widget_product_label_section_and_note.ProductLabelSectionAndNoteFieldAutocomplete";
setup() {
super.setup();
this.input = useRef("section_and_note_input");
}
get isSectionOrNote() {
return this.props.isSection || this.props.isNote;
}
get isSection() {
return this.props.isSection;
}
}
export class ProductLabelSectionAndNoteField extends Many2OneField {
static components = {
...Many2OneField.components,
Many2XAutocomplete: ProductLabelSectionAndNoteFieldAutocomplete,
};
static template =
"web_widget_product_label_section_and_note.ProductLabelSectionAndNoteField";
setup() {
super.setup();
this.isPrintMode = useState({value: false});
this.labelVisibility = useState({value: false});
this.isProductVisible = useState({value: false});
this.switchToLabel = false;
this.changeProductVisibility = true;
this.columnIsProductAndLabel = useState({
value: this.props.record.columnIsProductAndLabel,
});
this.labelNode = useRef("labelNodeRef");
useProductAndLabelAutoresize(this.labelNode, {
targetParentName: this.props.name,
});
this.productNode = useRef("productNodeRef");
useProductAndLabelAutoresize(this.productNode, {
targetParentName: this.props.name,
});
useEffect(
() => {
this.columnIsProductAndLabel.value =
this.props.record.columnIsProductAndLabel;
},
() => [this.props.record.columnIsProductAndLabel]
);
onPatched(() => {
if (this.labelNode.el && this.switchToLabel) {
this.switchToLabel = false;
this.labelNode.el.focus();
}
});
this.onBeforePrint = () => {
this.isPrintMode.value = true;
};
this.onAfterPrint = () => {
this.isPrintMode.value = false;
};
// The following hooks are used to make a div visible only in the print view. This div is necessary in the
// print view in order not to have scroll bars but can't be displayed in the normal view because it adds
// an empty line. This is done by switching an attribute to true only during the print view life cycle and
// including the said div in a t-if depending on that attribute.
onMounted(() => {
window.addEventListener("beforeprint", this.onBeforePrint);
window.addEventListener("afterprint", this.onAfterPrint);
});
onWillUnmount(() => {
window.removeEventListener("beforeprint", this.onBeforePrint);
window.removeEventListener("afterprint", this.onAfterPrint);
});
useRecordObserver(async (record) => {
if (this.changeProductVisibility) {
const label = record.data.name || "";
this.isProductVisible.value = label.includes(this.productName);
}
});
}
get productName() {
return this.props.record.data[this.props.name][1];
}
get label() {
let label = this.props.record.data.name || "";
if (label.includes(this.productName)) {
label = label.replace(this.productName, "");
if (label.includes("\n")) {
label = label.replace("\n", "");
}
}
return label;
}
get Many2XAutocompleteProps() {
const props = super.Many2XAutocompleteProps;
props.isSection = this.isSection(this.props.record);
props.isNote = this.isNote(this.props.record);
props.placeholder = _t("Search a product");
props.updateLabel = this.updateLabel.bind(this);
return props;
}
get isProductClickable() {
return this.props.record.evalContext.parent.state !== "draft";
}
get isSectionOrNote() {
return this.isSection(this.props.record) || this.isNote(this.props.record);
}
get sectionAndNoteClasses() {
if (this.isSection()) {
return "fw-bold";
} else if (this.isNote()) {
return "fst-italic";
}
return "";
}
isSection(record = null) {
record = record || this.props.record;
return record.data.display_type === "line_section";
}
isNote(record = null) {
record = record || this.props.record;
return record.data.display_type === "line_note";
}
switchLabelVisibility() {
this.labelVisibility.value = !this.labelVisibility.value;
this.switchToLabel = true;
}
switchProductVisibility() {
let new_name = "";
if (this.isProductVisible.value && this.productName) {
new_name = this.label;
} else {
new_name = this.productName + "\n" + this.label;
}
this.props.record.update({name: new_name});
this.isProductVisible.value = !this.isProductVisible.value;
}
updateLabel(value) {
this.changeProductVisibility = false;
this.props.record.update({
name:
this.productName &&
this.productName !== value &&
this.isProductVisible.value
? `${this.productName}\n${value}`
: value,
});
}
}
export const productLabelSectionAndNoteField = {
...Many2OneField,
component: ProductLabelSectionAndNoteField,
};
registry
.category("fields")
.add("product_label_section_and_note_field", productLabelSectionAndNoteField);

View File

@ -0,0 +1,5 @@
.o_field_product_label_section_and_note_cell {
textarea {
resize: none;
}
}

View File

@ -0,0 +1,218 @@
<?xml version="1.0" encoding="UTF-8" ?>
<templates>
<t
t-name="web_widget_product_label_section_and_note.ProductLabelSectionAndNoteField"
>
<div class="o_field_product_label_section_and_note_cell">
<t t-if="props.readonly">
<t t-if="!isSectionOrNote">
<t t-if="!isProductClickable">
<span
t-if="productName"
t-ref="productNodeRef"
class="d-flex align-items-center justify-content-between w-100"
>
<span class="text-truncate">
<t t-out="productName" />
</span>
</span>
<span
t-if="!productName and (columnIsProductAndLabel.value and !label)"
t-ref="productNodeRef"
class="d-flex align-items-center justify-content-between text-muted"
>
<span class="text-truncate">Search a product</span>
</span>
<t t-if="columnIsProductAndLabel.value and label">
<textarea
class="o_input d-print-none border-0 fst-italic"
rows="1"
type="text"
t-att-class="sectionAndNoteClasses"
t-att-value="label"
t-on-change="(ev) => this.updateLabel(ev.target.value)"
t-ref="labelNodeRef"
/>
</t>
</t>
<t t-else="">
<a
t-if="(columnIsProductAndLabel.value and value) or (!columnIsProductAndLabel.value and (productName or (!productName and !label)))"
class="d-flex align-items-center justify-content-between"
t-attf-class="o_form_uri #{classFromDecoration}"
t-att-href="value ? `/odoo/${urlRelation}/${value[0]}` : '/'"
t-on-click.prevent="onClick"
>
<span t-ref="productNodeRef" class="w-100">
<span t-esc="productName" class="text-truncate" />
</span>
</a>
<textarea
t-if="(columnIsProductAndLabel.value and label) or (!columnIsProductAndLabel.value and !productName and label)"
class="o_input d-print-none border-0 fst-italic"
placeholder="Enter a description"
readonly="1"
rows="1"
type="text"
t-att-class="sectionAndNoteClasses"
t-att-value="label"
t-on-change="(ev) => this.updateLabel(ev.target.value)"
t-ref="labelNodeRef"
/>
</t>
</t>
<t t-else="">
<t t-if="props.isSection">
<input
class="o_input text-wrap border-0 fst-italic"
placeholder="Enter a description"
type="text"
t-att-class="sectionAndNoteClasses"
t-att-value="label"
t-on-change="(ev) => this.updateLabel(ev.target.value)"
t-ref="labelNodeRef"
/>
</t>
<t t-else="">
<textarea
class="o_input d-print-none border-0 fst-italic"
placeholder="Enter a description"
rows="1"
type="text"
t-att-class="sectionAndNoteClasses"
t-att-value="label"
t-on-change="(ev) => this.updateLabel(ev.target.value)"
t-ref="labelNodeRef"
/>
</t>
</t>
</t>
<t t-else="">
<t t-if="!isSectionOrNote">
<div class="d-flex align-items-center justify-content-between">
<Many2XAutocomplete t-props="Many2XAutocompleteProps" />
<button
t-if="hasExternalButton"
aria-label="Internal link"
class="btn btn-link text-action oi o_external_button"
data-tooltip="Internal link"
draggable="false"
tabindex="-1"
type="button"
t-att-class="env.inDialog ? 'oi-launch' : 'oi-arrow-right'"
t-on-click="onExternalBtnClick"
/>
<button
t-if="hasBarcodeButton"
aria-label="Scan barcode"
class="btn ms-3 o_barcode o_external_button px-2"
data-tooltip="Scan barcode"
draggable="false"
tabindex="-1"
title="Scan barcode"
type="button"
t-on-click="onBarcodeBtnClick"
/>
<button
t-if="columnIsProductAndLabel.value and !label"
class="btn fa fa-bars text-start o_external_button px-1"
data-tooltip="Click or press enter to add a description"
id="labelVisibilityButtonId"
type="button"
t-on-click="switchLabelVisibility"
/>
<button
t-if="columnIsProductAndLabel.value and label"
t-att-class="'btn text-start o_external_button px-1 fa ' + ( this.isProductVisible.value ? 'fa-eye' : 'fa-eye-slash')"
data-tooltip="Click to remove/add the product name from the description."
id="ProductVisibilityButtonId"
type="button"
t-on-click="switchProductVisibility"
/>
</div>
<t
t-if="columnIsProductAndLabel.value and (label or labelVisibility.value)"
>
<textarea
class="o_input d-print-none border-0 fst-italic"
placeholder="Enter a description"
rows="1"
type="text"
t-att-class="sectionAndNoteClasses"
t-att-value="label"
t-on-change="(ev) => this.updateLabel(ev.target.value)"
t-ref="labelNodeRef"
/>
</t>
</t>
<t t-else="">
<t t-if="props.isSection">
<input
class="o_input text-wrap border-0 fst-italic"
type="text"
t-att-class="sectionAndNoteClasses"
t-att-value="label"
t-on-change="(ev) => this.updateLabel(ev.target.value)"
t-ref="labelNodeRef"
/>
</t>
<t t-else="">
<textarea
class="o_input d-print-none border-0 fst-italic"
rows="1"
type="text"
t-att-class="sectionAndNoteClasses"
t-att-value="label"
t-on-change="(ev) => this.updateLabel(ev.target.value)"
t-ref="labelNodeRef"
/>
</t>
</t>
</t>
<t t-if="isPrintMode.value">
<div class="d-none d-print-block text-wrap" t-out="label" />
</t>
</div>
</t>
<t
t-name="web_widget_product_label_section_and_note.ProductLabelSectionAndNoteFieldAutocomplete"
t-inherit="web.Many2XAutocomplete"
t-inherit-mode="primary"
>
<xpath expr="//AutoComplete[@t-else='']" position="replace">
<t t-else="">
<t t-if="!isSectionOrNote">
<AutoComplete
autofocus="props.autofocus"
autoSelect="props.autoSelect"
dropdown="props.dropdown"
id="props.id"
onChange.bind="onChange"
onInput.bind="onInput"
onSelect.bind="onSelect"
placeholder="props.placeholder"
resetOnSelect="props.value === ''"
sources="sources"
value="props.value"
/>
</t>
<t t-else="">
<input
class="o_input"
type="text"
t-att-id="props.id"
t-att-value="props.value"
t-on-keydown="onKeyDown"
t-ref="section_and_note_input"
/>
</t>
</t>
</xpath>
<xpath expr="//span[hasclass('o_dropdown_button')]" position="attributes">
<attribute name="t-if">!isSectionOrNote</attribute>
</xpath>
</t>
</templates>

View File

@ -0,0 +1,81 @@
/** @odoo-module **/
/* Copyright Odoo S.A.
* Copyright 2024 Tecnativa - Carlos Lopez
* License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). */
import {useEffect} from "@odoo/owl";
import {browser} from "@web/core/browser/browser";
import {resizeTextArea} from "@web/core/utils/autoresize";
function resizeInput(input) {
// This mesures the maximum width of the input which can get from the flex layout.
input.style.width = "100%";
const maxWidth = input.clientWidth;
// Somehow Safari 16 computes input sizes incorrectly. This is fixed in Safari 17
const isSafari16 = /Version\/16.+Safari/i.test(browser.navigator.userAgent);
// Minimum width of the input
input.style.width = "10px";
if (input.value === "" && input.placeholder !== "") {
input.style.width = "auto";
return;
}
if (input.scrollWidth + 5 + (isSafari16 ? 8 : 0) > maxWidth) {
input.style.width = "100%";
return;
}
input.style.width = input.scrollWidth + 5 + (isSafari16 ? 8 : 0) + "px";
}
/**
* This is used on text inputs or textareas to automatically resize it based on its
* content each time it is updated. It takes the reference of the element as
* parameter and some options. Do note that it may introduce mild performance issues
* since it will force a reflow of the layout each time the element is updated.
* Do also note that it only works with textareas that are nested as only child
* of some parent div (like in the text_field component).
*
* @param {Ref} ref
*/
export function useAutoresize(ref, options = {}) {
let wasProgrammaticallyResized = false;
let resize = null;
useEffect(
(el) => {
if (el) {
resize = (programmaticResize = false) => {
wasProgrammaticallyResized = programmaticResize;
if (el instanceof HTMLInputElement) {
resizeInput(el, options);
} else {
resizeTextArea(el, options);
}
if (options.onResize) {
options.onResize(el, options);
}
};
el.addEventListener("input", () => resize(true));
const resizeObserver = new ResizeObserver(() => {
// This ensures that the resize function is not called twice on input or page load
if (wasProgrammaticallyResized) {
wasProgrammaticallyResized = false;
return;
}
resize();
});
resizeObserver.observe(el);
return () => {
el.removeEventListener("input", resize);
resizeObserver.unobserve(el);
resizeObserver.disconnect();
resize = null;
};
}
},
() => [ref.el]
);
useEffect(() => {
if (resize) {
resize(true);
}
});
}

View File

@ -0,0 +1,41 @@
/** @odoo-module **/
/* Copyright Odoo S.A.
* Copyright 2024 Tecnativa - Carlos Lopez
* License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). */
import {useAutoresize} from "./autoresize.esm";
export function productAndLabelResizeTextArea(textarea, options = {}) {
const style = window.getComputedStyle(textarea);
if (options.targetParentName) {
let target = textarea.parentElement;
let shouldContinue = true;
while (target && shouldContinue) {
const totalParentHeight = Array.from(target.children).reduce(
(total, child) => {
const childHeight = child.style.height || style.lineHeight;
return total + parseFloat(childHeight);
},
0
);
target.style.height = `${totalParentHeight}px`;
if (target.getAttribute("name") === options.targetParentName) {
shouldContinue = false;
}
target = target.parentElement;
}
}
}
/**
* This overriden version of the resizeTextArea method is specificly done for the product_label_section_and_note widget
* His necessity is found in the fact that the cell of said widget doesn't contain only the input or textarea to resize
* but also another node containing the name of the product if said data is available. This means that the autoresize
* method which sets the height of the parent cell should sometimes add an additional row to the parent cell so that
* no text overflows
*
* @param {Ref} ref
*/
export function useProductAndLabelAutoresize(ref, options = {}) {
useAutoresize(ref, {onResize: productAndLabelResizeTextArea, ...options});
}

View File

@ -0,0 +1,25 @@
<?xml version="1.0" encoding="utf-8" ?>
<odoo>
<record id="view_account_move_form" model="ir.ui.view">
<field name="name">account.move.view.form</field>
<field name="model">account.move</field>
<field name="inherit_id" ref="account.view_move_form" />
<field name="arch" type="xml">
<xpath expr="//field[@name='invoice_line_ids']" position="attributes">
<attribute
name="widget"
>product_label_section_and_note_field_o2m</attribute>
</xpath>
<xpath
expr="//field[@name='invoice_line_ids']/tree//field[@name='product_id']"
position="attributes"
>
<attribute
name="widget"
>product_label_section_and_note_field</attribute>
</xpath>
</field>
</record>
</odoo>