Compare commits
9 Commits
pipu-odoo-
...
16.0-updat
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
3205d211f2 |
[IMP] create JavaScript howtos
The JavaScript cheatsheet is outdated, we therefore remove it and replace it by multiple howtos: - Create a view from scratch - Extending an existing view - Create a field from scratch - Extend an existing field - Create a client action There is other subjects to introduce as the web framework is big. Other future contributions will cover them. |
||
|
|
cde27305ee |
[ADD] attendances: hr and attendances categories + hardware page
Task ID: 3251124
closes odoo/documentation#3956
X-original-commit:
|
||
|
|
cd1f3e4510 |
[IMP] ePoS: vulgarise the SSL ePos issue
SSL/HTTPS topic is complicated for most of
Odoo customers as it is quite technical.
This PR should help them guide them to better
understand the issue and how to fix it themselves.
Support can't be provided to each device, browsers and OS.
But we did add some guides regarding the more
"popular" ones and some "keyword" to search
online for the others.
closes odoo/documentation#3922
X-original-commit:
|
||
|
|
9b0a54b7f2 |
[IMP] Adyen: additional minimum requirements for users
Adding requirements for users to use Adyen. Forward to master.
closes odoo/documentation#3952
Taskid: 3159712
X-original-commit:
|
||
|
|
a1fce64ce5 |
[IMP] mail plugins: add instructions to gmail plugin
closes odoo/documentation#3944
X-original-commit:
|
||
|
|
fa90d23a90 |
[IMP] sales: menuselection fix
Fixed a menuselection error and deleted instances of second-person pov
Closes task 3116083
closes odoo/documentation#3939
X-original-commit:
|
||
|
|
098ecbcb99 |
[IMP] General: Oauth seemore additions
closes odoo/documentation#3934
X-original-commit:
|
||
|
|
4a7acf8c02 |
[IMP] pos: update fiscal positions page
Task ID: 2862506 closes odoo/documentation#3876 Signed-off-by: Castillo Jonathan (jcs) <jcs@odoo.com> |
||
|
|
de8db5a6a0 |
[IMP] paypal: remove deleted field
Removing no longer used Merchant Account ID field. task-2854184 closes odoo/documentation#3833 Signed-off-by: Antoine Vandevenne (anv) <anv@odoo.com> |
|
|
@@ -11,6 +11,10 @@ personal email address or an address created by a custom domain.
|
|||
`Microsoft Learn: Register an application with the Microsoft identity platform
|
||||
<https://learn.microsoft.com/azure/active-directory/develop/quickstart-register-app>`_
|
||||
|
||||
.. seealso::
|
||||
- :doc:`/applications/general/auth/azure`
|
||||
- :doc:`/applications/general/calendars/outlook/outlook_calendar`
|
||||
|
||||
Setup in Microsoft Azure Portal
|
||||
===============================
|
||||
|
||||
|
|
|
|||
|
|
@@ -11,6 +11,10 @@ email address or an address created by a custom domain.
|
|||
For more information, visit `Google's documentation
|
||||
<https://support.google.com/cloud/answer/6158849>`_ on setting up OAuth.
|
||||
|
||||
.. seealso::
|
||||
- :doc:`/applications/general/auth/google`
|
||||
- :doc:`/applications/general/calendars/google/google_calendar_credentials`
|
||||
|
||||
Setup in Google
|
||||
===============
|
||||
|
||||
|
|
|
|||
|
|
@@ -18,6 +18,7 @@ Discover our user guides and configuration tutorials per application.
|
|||
applications/sales
|
||||
applications/websites
|
||||
applications/inventory_and_mrp
|
||||
applications/hr
|
||||
applications/marketing
|
||||
applications/services
|
||||
applications/productivity
|
||||
|
|
|
|||
|
|
@@ -26,6 +26,8 @@ They can be applied in various ways:
|
|||
Configuration
|
||||
=============
|
||||
|
||||
.. _fiscal_positions/mapping:
|
||||
|
||||
Tax and Account Mapping
|
||||
-----------------------
|
||||
|
||||
|
|
|
|||
|
|
@@ -5,6 +5,14 @@ Adyen
|
|||
`Adyen <https://www.adyen.com/>`_ is a Dutch company that offers several online payment
|
||||
possibilities.
|
||||
|
||||
.. seealso::
|
||||
- :ref:`payment_providers/add_new`
|
||||
- :doc:`../payment_providers`
|
||||
|
||||
.. note::
|
||||
Adyen works only with customers processing **more** than **10 million annually** or invoicing a
|
||||
**minimum** of **1.000** transactions **per month**.
|
||||
|
||||
Configuration
|
||||
=============
|
||||
|
||||
|
|
|
|||
|
|
@@ -19,7 +19,6 @@ Credentials tab
|
|||
Odoo needs your **API Credentials** to connect with your PayPal account, which comprise:
|
||||
|
||||
- **Email**: your login email address in Paypal.
|
||||
- **Merchant Account ID**: the code of the merchant account used to identify your Paypal account.
|
||||
- **PDT Identity Token**: the key used to verify the authenticity of transactions.
|
||||
- **Use IPN**: whether you want to use Instant Payment Notification. Already checked, you don't have
|
||||
to change it.
|
||||
|
|
@@ -27,8 +26,6 @@ Odoo needs your **API Credentials** to connect with your PayPal account, which c
|
|||
You can copy your credentials from your Paypal account and paste them into the related fields under
|
||||
the **Credentials** tab.
|
||||
|
||||
To retrieve the **Merchant Account ID**, log into your Paypal account and go to
|
||||
:menuselection:`Account menu --> Account Settings --> Business information`.
|
||||
|
||||
To set the **PDT Identity Token**, switch to :ref:`developer mode <developer-mode>` and retrieve the
|
||||
token by following the configuration step :ref:`paypal/enable-pdt`.
|
||||
|
|
|
|||
|
|
@@ -1,6 +1,10 @@
|
|||
=====
|
||||
OAuth
|
||||
=====
|
||||
======================================
|
||||
Microsoft Azure sign-in authentication
|
||||
======================================
|
||||
|
||||
Due to specific requirements in Azure's OAuth implementation,
|
||||
Microsoft Azure OAuth identification is NOT compatible with Odoo at the moment.
|
||||
Due to specific requirements in Azure's OAuth implementation, Microsoft Azure OAuth identification
|
||||
is NOT compatible with Odoo at the moment.
|
||||
|
||||
.. seealso::
|
||||
- :doc:`/applications/general/calendars/outlook/outlook_calendar`
|
||||
- :doc:`/administration/maintain/azure_oauth`
|
||||
|
|
|
|||
|
|
@@ -8,6 +8,10 @@ with their Google account.
|
|||
This is particularly helpful if your organization uses Google Workforce and you want the employees
|
||||
within your organization to connect to Odoo with their Google Accounts.
|
||||
|
||||
.. seealso::
|
||||
- :doc:`/applications/general/calendars/google/google_calendar_credentials`
|
||||
- :doc:`/administration/maintain/google_oauth`
|
||||
|
||||
.. _google-sign-in/configuration:
|
||||
|
||||
Configuration
|
||||
|
|
@@ -42,7 +46,7 @@ OAuth consent screen
|
|||
:align: center
|
||||
:alt: Google oauth consent selection menu
|
||||
|
||||
#. Choose one of the options **(Internal / External)** as instructed, and click on *Create*.
|
||||
#. Choose the option for :guilabel:`internal`, and click on :guilabel:`Create`.
|
||||
|
||||
.. image:: google/consent.png
|
||||
:align: center
|
||||
|
|
|
|||
|
|
@@ -5,6 +5,10 @@ Synchronize Google Calendar with Odoo
|
|||
Synchronize Google Calendar with Odoo to see and manage meetings from both platforms (updates go
|
||||
in both directions). This integration helps organize your schedule so you never miss a meeting.
|
||||
|
||||
.. seealso::
|
||||
- :doc:`/applications/general/auth/google`
|
||||
- :doc:`/administration/maintain/google_oauth`
|
||||
|
||||
Setup in Google
|
||||
===============
|
||||
|
||||
|
|
|
|||
|
|
@@ -5,6 +5,10 @@ Synchronize Outlook Calendar with Odoo
|
|||
Synchronizing a user's Outlook Calendar with Odoo is useful for keeping track of their tasks and
|
||||
appointments across all related applications.
|
||||
|
||||
.. seealso::
|
||||
- :doc:`/applications/general/auth/azure`
|
||||
- :doc:`/administration/maintain/azure_oauth`
|
||||
|
||||
Register the application with Microsoft Azure
|
||||
=============================================
|
||||
|
||||
|
|
|
|||
9
content/applications/hr.rst
Normal file
|
|
@@ -0,0 +1,9 @@
|
|||
:nosearch:
|
||||
|
||||
===============
|
||||
Human resources
|
||||
===============
|
||||
|
||||
.. toctree::
|
||||
|
||||
hr/attendances
|
||||
19
content/applications/hr/attendances.rst
Normal file
|
|
@@ -0,0 +1,19 @@
|
|||
:nosearch:
|
||||
:show-content:
|
||||
:hide-page-toc:
|
||||
:show-toc:
|
||||
|
||||
===========
|
||||
Attendances
|
||||
===========
|
||||
|
||||
**Odoo Attendances** functions as a time clock. Employees check in and check out of work, while
|
||||
managers can see who is available at any given time.
|
||||
|
||||
.. seealso::
|
||||
`Odoo Tutorials: Attendances <https://www.odoo.com/slides/slide/attendances-684>`_
|
||||
|
||||
.. toctree::
|
||||
:titlesonly:
|
||||
|
||||
attendances/hardware
|
||||
57
content/applications/hr/attendances/hardware.rst
Normal file
|
|
@@ -0,0 +1,57 @@
|
|||
========
|
||||
Hardware
|
||||
========
|
||||
|
||||
Kiosk management
|
||||
================
|
||||
|
||||
A kiosk is a self-service station that allows employees to check in and check out for work shifts.
|
||||
|
||||
There are two ways to set up a kiosk:
|
||||
|
||||
- **Laptop and desktop PC**
|
||||
|
||||
Running a kiosk in a web browser is the cheapest and most flexible option. You can print employee
|
||||
badges with any thermal or inkjet printer compatible with your web browser.
|
||||
|
||||
- **Tablet and mobile phone (Android or iOS)**
|
||||
|
||||
Tablets and mobile phones take up much less space, and their touchscreens are easy to use.
|
||||
Consider putting them in a secure stand at the front desk or mounting them securely on a wall.
|
||||
|
||||
.. tip::
|
||||
We recommend using an iPad together with the `Heckler Design WindFall Stand
|
||||
<https://hecklerdesign.com/products/windfall-stand-for-ipad>`_
|
||||
|
||||
RFID key fob readers
|
||||
====================
|
||||
|
||||
Employees can scan personal RFID key fobs with an RFID reader to manage check-ins and check-outs
|
||||
quickly and easily.
|
||||
|
||||
.. image:: hardware/rfid-reader.jpg
|
||||
:align: center
|
||||
:width: 40%
|
||||
:alt: An RFID key fob is placed on an RFID reader
|
||||
|
||||
.. tip::
|
||||
We recommend using the `Neuftech USB RFID Reader <https://neuftech.net/Neuftech-USB-RFID-Reader-ID-Kartenleseger%C3%A4t-Kartenleser-Kontaktlos-Card-Reader-f%C3%BCr-EM4100>`_.
|
||||
|
||||
.. note::
|
||||
An IoT box is **not** required.
|
||||
|
||||
Barcode scanners
|
||||
================
|
||||
|
||||
Employees can scan the barcode on their employee badges to manage check-ins and check-outs quickly
|
||||
and easily. The kiosk mode works with most USB barcode scanners connected directly to a computer.
|
||||
Bluetooth barcode scanners are also supported natively.
|
||||
|
||||
.. tip::
|
||||
We recommend using the `Honeywell product line
|
||||
<https://sps.honeywell.com/us/en/products/productivity/barcode-scanners>`_. If the barcode
|
||||
scanner is connected directly to a computer, it must be configured to use the computer's keyboard
|
||||
layout.
|
||||
|
||||
.. note::
|
||||
An IoT box is **not** required.
|
||||
BIN
content/applications/hr/attendances/hardware/rfid-reader.jpg
Normal file
|
After Width: | Height: | Size: 34 KiB |
|
|
@@ -116,15 +116,18 @@ Delete the following three lines of text from the :file:`login.ts` file:
|
|||
This removes the `odoo.com` domain constraint from the Gmail Plugin program.
|
||||
|
||||
Next, in the ZIP file, go to :menuselection:`mail-client-extensions-master --> gmail`, and open the
|
||||
file called :guilabel:`README`. Follow the instructions in the :guilabel:`README` file to push the
|
||||
Gmail Plugin files as a Google Project.
|
||||
file called :guilabel:`appsscript.json`. In the :guilabel:`urlFetchWhitelist` section, replace all
|
||||
the references to `odoo.com` with the Odoo customer's unique server domain.
|
||||
|
||||
Then, in the same :guilabel:`gmail` folder, open the file called :guilabel:`README.md`. Follow the
|
||||
instructions in the :guilabel:`README.md` file to push the Gmail Plugin files as a Google Project.
|
||||
|
||||
.. note::
|
||||
The computer must be able to run Linux commands in order to follow the instructions on the
|
||||
:guilabel:`README` file.
|
||||
:guilabel:`README.md` file.
|
||||
|
||||
After that, share the Google Project with the Gmail account that the user wishes to connect to
|
||||
Odoo. Then, click :guilabel:`Publish` and :guilabel:`Deploy from manifest`. Lastly, click
|
||||
After that, share the Google Project with the Gmail account that the user wishes to connect to Odoo.
|
||||
Then, click :guilabel:`Publish` and :guilabel:`Deploy from manifest`. Lastly, click
|
||||
:guilabel:`Install the add-on` to install the Gmail Plugin.
|
||||
|
||||
Configure the Odoo database
|
||||
|
|
@@ -132,8 +135,7 @@ Configure the Odoo database
|
|||
|
||||
The :guilabel:`Mail Plugin` feature must be enabled in the Odoo database in order to use the Gmail
|
||||
Plugin. To enable the feature, go to :menuselection:`Settings --> General Settings`. Under the
|
||||
:guilabel:`Integrations` section, activate :guilabel:`Mail Plugin`, and then click
|
||||
:guilabel:`Save`.
|
||||
:guilabel:`Integrations` section, activate :guilabel:`Mail Plugin`, and then click :guilabel:`Save`.
|
||||
|
||||
.. image:: gmail/mail-plugin-setting.png
|
||||
:align: center
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 7.3 KiB After Width: | Height: | Size: 7.9 KiB |
|
Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 39 KiB |
|
|
@@ -2,61 +2,61 @@
|
|||
Multiple sales teams
|
||||
====================
|
||||
|
||||
Odoo lets you manage several sales teams, departments, or channels, each with their own unique
|
||||
sales processes, using *Sales Teams*.
|
||||
Use the *Sales Teams* feature to manage several sales teams, departments, or channels, each with
|
||||
their own unique sales processes.
|
||||
|
||||
|
||||
Create a new Sales Team
|
||||
Create a new sales team
|
||||
=======================
|
||||
|
||||
To create a new Sales Team, go to :menuselection:`CRM --> Sales --> Teams` then click
|
||||
:guilabel:`Create`.
|
||||
To create a new sales team, go to :menuselection:`CRM --> Configuration --> Sales Teams`, then
|
||||
click :guilabel:`Create`.
|
||||
|
||||
On the creation page, set an :guilabel:`Email Alias` to automatically generate a lead/opportunity
|
||||
for this Sales Team every time a message is sent to that unique email address. You can also choose
|
||||
whether to accept emails from :guilabel:`Everyone`, :guilabel:`Authenticated Partners`, or
|
||||
:guilabel:`Followers Only`.
|
||||
for this sales team every time a message is sent to that unique email address. Choose whether to
|
||||
accept emails from :guilabel:`Everyone`, :guilabel:`Authenticated Partners`, or :guilabel:`Followers
|
||||
Only`.
|
||||
|
||||
Set a :guilabel:`Domain` to assign leads/opportunities to this Sales Team based on specific
|
||||
filters, such as country, language, or campaign. Set an :guilabel:`Invoicing Target` if this team
|
||||
has specific monthly revenue goals.
|
||||
Set an :guilabel:`Invoicing Target` if this team has specific monthly revenue goals. Set a
|
||||
:guilabel:`Domain` to assign leads/opportunities to this sales team based on specific filters, such
|
||||
as country, language, or campaign.
|
||||
|
||||
.. image:: multi_sales_team/sales-team-creation.png
|
||||
:align: center
|
||||
:alt: Create a Sales Team in Odoo CRM.
|
||||
:alt: Create a sales team in Odoo CRM.
|
||||
|
||||
Add members to a Sales Team
|
||||
Add members to a sales team
|
||||
---------------------------
|
||||
|
||||
To add team members, click :guilabel:`Add` under the Assignment tab when editing the Sales Team's
|
||||
configuration page. Select a salesperson from the dropdown menu or create new salesperson. Set a
|
||||
maximum number of leads that can be assigned to this salesperson in a 30-day period to ensure that
|
||||
they do not overwork.
|
||||
To add team members, click :guilabel:`Add` under the :guilabel:`Members` tab when editing the sales
|
||||
team's configuration page. Select a salesperson from the drop-down menu or create new salesperson.
|
||||
Set a maximum number of leads that can be assigned to this salesperson in a 30-day period to ensure
|
||||
that they do not overwork.
|
||||
|
||||
.. image:: multi_sales_team/add-a-salesperson.png
|
||||
:align: center
|
||||
:alt: Add a Salesperson inside Odoo CRM.
|
||||
:alt: Add a salesperson in Odoo CRM.
|
||||
|
||||
One person can be added as a team member or Team Leader to multiple Sales Teams, allowing them to
|
||||
access all of the pipelines that they need to.
|
||||
One person can be added as a team member or :guilabel:`Team Leader` to multiple sales teams,
|
||||
allowing them to access all of the pipelines that they need to.
|
||||
|
||||
Sales Team dashboard
|
||||
Sales team dashboard
|
||||
====================
|
||||
|
||||
To view the Sales Team dashboard, go to :menuselection:`CRM --> Sales --> Teams`. Any teams you are
|
||||
a part of will appear as dashboard tiles.
|
||||
To view the sales team dashboard, go to :menuselection:`CRM --> Sales --> Teams`. Odoo users will
|
||||
see any teams that they are a part of as dashboard tiles.
|
||||
|
||||
Each tile gives an overview of the Sales Team's open opportunities, quotations, sales orders, and
|
||||
Each tile gives an overview of the sales team's open opportunities, quotations, sales orders, and
|
||||
expected revenue, as well as a bar graph of new opportunities per week and an invoicing progress
|
||||
bar.
|
||||
|
||||
.. image:: multi_sales_team/sales-team-overview.png
|
||||
:align: center
|
||||
:alt: Sales Team Overview dashboard in Odoo CRM.
|
||||
:alt: Sales team overview dashboard in Odoo CRM.
|
||||
|
||||
Click on the three dots in the corner of a tile to open a navigational menu that lets you quickly
|
||||
Click on the three dots in the corner of a tile to open a navigational menu that lets users quickly
|
||||
view documents or reports, create new quotations or opportunities, pick a color for this team, or
|
||||
access its configuration page.
|
||||
access the team's configuration page.
|
||||
|
||||
.. image:: multi_sales_team/team-overview-three-dot-menu.png
|
||||
:align: center
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 4.7 KiB After Width: | Height: | Size: 33 KiB |
|
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 52 KiB |
|
Before Width: | Height: | Size: 8.4 KiB After Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 6.1 KiB After Width: | Height: | Size: 30 KiB |
|
|
@@ -1,18 +1,16 @@
|
|||
.. _epos_ssc/ePOS printers:
|
||||
|
||||
=========================================
|
||||
Self-signed certificate for ePOS printers
|
||||
=========================================
|
||||
|
||||
ePos printers are designed specifically to work with your Point of Sale system, which sends the
|
||||
tickets directly to the printer.
|
||||
|
||||
Some models don't require an IoT box, but the connection between your web browser and the printer
|
||||
may require a :doc:`secure connection with the HTTPS protocol <https>`. If so, a self-signed
|
||||
certificate is necessary to use your printer.
|
||||
ePOS printers are designed to work seamlessly with Point of Sale systems. Once connected, the two
|
||||
devices automatically share information, enabling the direct printing of tickets from the POS system
|
||||
to the ePOS printer.
|
||||
|
||||
.. note::
|
||||
Please check the following list of compatible `Epson ePOS printers
|
||||
<https://c4b.epson-biz.com/modules/community/index.php?content_id=91>`_. This list includes the
|
||||
following models:
|
||||
These `Epson ePOS printers
|
||||
<https://c4b.epson-biz.com/modules/community/index.php?content_id=91>`_ are compatible with Odoo:
|
||||
|
||||
- TM-H6000IV-DT (Receipt printer only)
|
||||
- TM-T70II-DT
|
||||
|
|
@@ -31,56 +29,191 @@ certificate is necessary to use your printer.
|
|||
- TM-P60II (Peeler: Wi-Fi® model)
|
||||
- TM-P80 (Wi-Fi® model)
|
||||
|
||||
Generate a Self-signed certificate
|
||||
==================================
|
||||
To work with Odoo, some models that can be used without an
|
||||
:doc:`IoT box <../../../productivity/iot/config/connect>` may require :doc:`the HTTPS protocol
|
||||
<https>` to establish a secure connection between the browser and the printer. However, trying to
|
||||
reach the printer's IP address using HTTPS leads to a warning page on most web browsers.
|
||||
|
||||
Access your ePOS printer's settings with your web browser by navigating to its IP address, for
|
||||
example, `http://192.168.1.25`.
|
||||
|
||||
.. note::
|
||||
- The printer automatically prints the IP address during startup.
|
||||
- We recommend assigning a **fixed IP address** to the printer from the network router.
|
||||
|
||||
Go to :menuselection:`Authentication --> Certificate List` and create a new **Self-Signed
|
||||
Certificate**.
|
||||
|
||||
- **Common Name**: the IP address of the ePos Printer, for example, `192.168.1.25`
|
||||
- **Validity Period**: `10`
|
||||
|
||||
Create and reboot the printer, go to :menuselection:`Security --> SSL/TLS`, and check if
|
||||
**Selfsigned Certificate** is selected.
|
||||
|
||||
Export the Self-signed certificate
|
||||
==================================
|
||||
|
||||
To avoid having to accept the self-signed certificate several times, you can export it and then
|
||||
import it to your web browser or mobile device.
|
||||
|
||||
To do so, access your ePOS printer's settings with your web browser by navigating to its IP address,
|
||||
for example, `https://192.168.1.25`. Then, accept the self-signed certificate.
|
||||
|
||||
.. note::
|
||||
Note that the protocol is now **HTTPS**.
|
||||
|
||||
Click on :menuselection:`Connection is not secure --> Certificate is not valid`.
|
||||
|
||||
.. image:: epos_ssc/browser-warning.png
|
||||
.. figure:: epos_ssc/browser-https-insecure.png
|
||||
:align: center
|
||||
:alt: The web browser indicates that the connection to the printer is not secure.
|
||||
:alt: warning page about the connection privacy on Google Chrome
|
||||
|
||||
Go to the :guilabel:`Details` tab and click on :guilabel:`Export` Select X.509 in base 64 and save it.
|
||||
Warning page on Google Chrome, Windows 10
|
||||
|
||||
Import the Self-signed certificate to Windows (Using Chrome)
|
||||
============================================================
|
||||
In that case, you can temporarily force the connection by clicking :guilabel:`Advanced` and
|
||||
:guilabel:`Proceed to [IP address] (unsafe)`. Doing so allows you to reach the page in HTTPS and use
|
||||
the ePOS printer in Odoo as long as the browser window stays open.
|
||||
|
||||
In your Chrome browser, go to :menuselection:`Settings --> Privacy and security --> Security -->
|
||||
Manage certificates`
|
||||
.. note::
|
||||
The previous instructions apply to Google Chrome but are similar to other browsers.
|
||||
|
||||
Go to the :guilabel:`Authorities` tab and click on :guilabel:`Import` and select
|
||||
your previous file. Accept all warnings and restart your browser.
|
||||
.. warning::
|
||||
The connection is lost after closing the browser window. Therefore, this method should only be
|
||||
used as a **workaround** or as a pre-requisite for the :ref:`following instructions
|
||||
<epos_ssc/instructions>`.
|
||||
|
||||
Import the Self-signed certificate to your Android device
|
||||
=========================================================
|
||||
.. _epos_ssc/instructions:
|
||||
|
||||
On your Android device, open the settings and search for *certificate*. Then, click on **Certificate
|
||||
AC** (Install from device storage), and select the certificate.
|
||||
Generate, export, and import self-signed certificates
|
||||
=====================================================
|
||||
|
||||
For a long-term solution, you must generate a **self-signed certificate**. Then, export and import
|
||||
it into your browser.
|
||||
|
||||
.. important::
|
||||
**Generating** an SSL certificate should only be done **once**. If you create another
|
||||
certificate, devices using the previous one will lose HTTPS access.
|
||||
|
||||
.. tabs::
|
||||
|
||||
.. tab:: Windows 10 & Linux OS
|
||||
|
||||
.. tabs::
|
||||
|
||||
.. tab:: Generate a self-signed certificate
|
||||
|
||||
After forcing the connection, sign in using your printer credentials to access the ePOS
|
||||
printer settings. To sign in, enter `epson` in the :guilabel:`ID` field and your printer
|
||||
serial number in the :guilabel:`Password` field.
|
||||
|
||||
Click :guilabel:`Certificate List` in the :guilabel:`Authentication` section, and click
|
||||
:guilabel:`create` to generate a new **Self-Signed Certificate**. The :guilabel:`Common
|
||||
Name` should be automatically filled out. If not, fill it in with the printer IP address
|
||||
number. Select the years the certificate will be valid in the :guilabel:`Validity
|
||||
Period` field, click :guilabel:`Create`, and :guilabel:`Reset` or manually restart the
|
||||
printer.
|
||||
|
||||
The self-signed certificate is generated. Reload the page and click :guilabel:`SSL/TLS`
|
||||
in the :guilabel:`Security` section to ensure **Selfsigned Certificate** is correctly
|
||||
selected in the :guilabel:`Server Certificate` section.
|
||||
|
||||
.. tab:: Export a self-signed certificate
|
||||
|
||||
The export process is heavily dependent on the :abbr:`OS (Operating System)` and the
|
||||
browser. Start by accessing your ePOS printer settings on your web browser by navigating
|
||||
to its IP address, for example, `https://192.168.1.25`. Then, force the connection as
|
||||
explained in the :ref:`introduction <epos_ssc/ePOS printers>`.
|
||||
|
||||
If you are using **Google Chrome**,
|
||||
|
||||
#. click :guilabel:`Not secure` next to the search bar, and :guilabel:`Certificate is
|
||||
not valid`;
|
||||
|
||||
.. image:: epos_ssc/browser-warning.png
|
||||
:align: center
|
||||
:alt: Connection to the printer not secure button in Google Chrome browser.
|
||||
|
||||
#. go to the :guilabel:`Details` tab and click :guilabel:`Export`;
|
||||
#. add `.crt` at the end of the file name to ensure it has the correct extension;
|
||||
#. select :guilabel:`Base64-encoded ASCII, single certificate`, at the bottom of the
|
||||
pop-up window;
|
||||
#. save, and the certificate is exported.
|
||||
|
||||
.. warning::
|
||||
Make sure that the certificate ends with the extension `.crt`. Otherwise, some
|
||||
browsers might not see the file during the import process.
|
||||
|
||||
If you are using **Mozilla Firefox**,
|
||||
|
||||
#. click the **lock-shaped** icon on the left of the address bar;
|
||||
#. go to :menuselection:`Connection not secure --> More information --> Security tab
|
||||
--> View certificate`;
|
||||
|
||||
.. image:: epos_ssc/mozilla-not-secure.png
|
||||
:align: center
|
||||
:alt: Connection is not secure button in Mozilla Firefox browser
|
||||
|
||||
#. scroll down to the :guilabel:`Miscellaneous` section;
|
||||
#. click :guilabel:`PEM (cert)` in the :guilabel:`Download` section;
|
||||
#. save, and the certificate is exported.
|
||||
|
||||
.. tab:: Import a self-signed certificate
|
||||
|
||||
The import process is heavily dependent on the :abbr:`OS (Operating System)` and the
|
||||
browser.
|
||||
|
||||
.. tabs::
|
||||
|
||||
.. tab:: Windows 10
|
||||
|
||||
Windows 10 manages certificates, which means that self-signed certificates must be
|
||||
imported from the certification file rather than the browser. To do so,
|
||||
|
||||
#. open the Windows File Explorer and locate the downloaded certification file;
|
||||
#. right-click on the certification file and click :guilabel:`Install
|
||||
Certificate`;
|
||||
#. select where to install the certificate and for whom - either for the
|
||||
:guilabel:`Current User` or all users (:guilabel:`Local Machine`). Then, click
|
||||
:guilabel:`Next`;
|
||||
#. on the `Certificate Store` screen, tick :guilabel:`Place all certificates in
|
||||
the following store`, click :guilabel:`Browse...`, and select
|
||||
:guilabel:`Trusted Root Certification Authorities`;
|
||||
|
||||
.. image:: epos_ssc/win-cert-wizard-store.png
|
||||
:align: center
|
||||
|
||||
#. click :guilabel:`Finish`, accept the pop-up security window;
|
||||
#. restart the computer to make sure that the changes are applied.
|
||||
|
||||
.. tab:: Linux
|
||||
|
||||
If you are using **Google Chrome**,
|
||||
|
||||
#. open Chrome;
|
||||
#. go to :menuselection:`Settings --> Privacy and security --> Security -->
|
||||
Manage certificates`;
|
||||
#. go to the :guilabel:`Authorities` tab, click :guilabel:`Import`, and select
|
||||
the exported certification file;
|
||||
#. accept all warnings;
|
||||
#. click :guilabel:`ok`;
|
||||
#. restart your browser.
|
||||
|
||||
|
||||
If you are using **Mozilla Firefox**,
|
||||
|
||||
#. open Firefox;
|
||||
#. go to :menuselection:`Settings --> Privacy & Security --> Security --> View
|
||||
Certificates... --> Import`;
|
||||
#. select the exported certification file;
|
||||
#. tick the checkboxes and validate;
|
||||
#. restart your browser.
|
||||
|
||||
.. tab:: Mac OS
|
||||
|
||||
To secure the connection on a Mac:
|
||||
|
||||
#. open Safari and navigate to your printer's IP address. Doing so leads to a warning page;
|
||||
#. on the warning page, go to :menuselection:`Show Details --> visit this website --> Visit
|
||||
Website`, validate;
|
||||
#. reboot the printer so you can use it with any other browser.
|
||||
|
||||
.. tab:: Android OS
|
||||
|
||||
To import an SSL certificate into an Android device, first create and export it from a
|
||||
computer. Next, transfer the `.crt` file to the device using email, Bluetooth, or USB. Once
|
||||
the file is on the device,
|
||||
|
||||
#. open the settings and search for `certificate`;
|
||||
#. click :guilabel:`Certificate AC` (Install from device storage);
|
||||
#. select the certificate file to install it on the device.
|
||||
|
||||
.. Note::
|
||||
The specific steps for installing a certificate may vary depending on the version of
|
||||
Android and the device manufacturer.
|
||||
|
||||
.. important::
|
||||
|
||||
- If you need to export SSL certificates from an operating system or web browser that has not
|
||||
been mentioned, search for `export SSL certificate` + `the name of your browser or operating
|
||||
system` in your preferred search engine.
|
||||
- Similarly, to import SSL certificates from an unmentioned OS or browser, search for `import SSL
|
||||
certificate root authority` + `the name of your browser or operating system` in your preferred
|
||||
search engine.
|
||||
|
||||
Check if the certificate was imported correctly
|
||||
===============================================
|
||||
|
||||
To confirm your printer's connection is secure, connect to its IP address using HTTPS. For example,
|
||||
navigate to `https://192.168.1.25` in your browser. If the SSL certificate has been applied
|
||||
correctly, you should no longer see a warning page, and the address bar should display a padlock
|
||||
icon, indicating that the connection is secure.
|
||||
|
|
|
|||
|
After Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 17 KiB |
|
After Width: | Height: | Size: 8.6 KiB |
|
After Width: | Height: | Size: 21 KiB |
|
|
@@ -1,43 +1,51 @@
|
|||
============================
|
||||
Tax rates (fiscal positions)
|
||||
============================
|
||||
=================================
|
||||
Flexible taxes (fiscal positions)
|
||||
=================================
|
||||
|
||||
In Odoo, *Fiscal Positions* let you apply different taxes based on
|
||||
the customer location. In a *Point of Sale*, such as a restaurant, it can
|
||||
be used to apply different taxes depending if the customer eats in or
|
||||
takes away.
|
||||
When running a business, you may need to apply different taxes and record transactions on various
|
||||
accounts based on the location and type of business of your customers and providers.
|
||||
|
||||
Set up fiscal positions for PoS
|
||||
===============================
|
||||
The **fiscal positions** feature enables you to establish rules that automatically select the right
|
||||
taxes and accounts used for each transaction.
|
||||
|
||||
To enable this feature, go to :menuselection:`Point of Sale --> Configuration --> Point of Sale`
|
||||
and check *Fiscal Position per Order*. Now, you can choose the fiscal positions
|
||||
you want for your *PoS*.
|
||||
.. seealso::
|
||||
- :doc:`../../../finance/accounting/taxation/taxes/fiscal_positions`
|
||||
- :doc:`../../../finance/accounting/taxation/taxes/taxes`
|
||||
|
||||
.. image:: fiscal_position/fiscal_position_01.png
|
||||
:align: center
|
||||
Configuration
|
||||
=============
|
||||
|
||||
To enable the feature, go to :menuselection:`Point of Sale --> Configuration --> Settings`, scroll
|
||||
down to the :guilabel:`Accounting` section, and enable :guilabel:`Flexible Taxes`.
|
||||
|
||||
Then, set a default fiscal position that should be applied to all sales in the selected POS in the
|
||||
:guilabel:`Default` field. You can also add more fiscal positions to choose from in the
|
||||
:guilabel:`Allowed` field.
|
||||
|
||||
.. image:: fiscal_position/flexible-taxes-setting.png
|
||||
:align: center
|
||||
|
||||
According to the :doc:`fiscal localization package <../../../finance/fiscal_localizations>`
|
||||
activated, several fiscal positions are preconfigured and can be set and used in POS. However, you
|
||||
can also :ref:`create new fiscal positions <fiscal_positions/mapping>`.
|
||||
|
||||
.. note::
|
||||
You need to create your fiscal positions before using this feature.
|
||||
If you do not set a fiscal position, the tax remains as defined in the **customer taxes** field
|
||||
on the product form.
|
||||
|
||||
Using fiscal positions
|
||||
======================
|
||||
Use fiscal positions
|
||||
====================
|
||||
|
||||
Once on your *PoS* interface, click on the *Tax* button.
|
||||
Now, choose the fiscal position you need for the current order.
|
||||
Open a :ref:`POS session <pos/start-session>` to use one of the allowed fiscal positions. Then,
|
||||
click the :guilabel:`Tax` button next to the **book-shaped** icon and select a fiscal position from
|
||||
the list. Doing so applies the defined rules automatically to all the products subject to the chosen
|
||||
fiscal position's regulations.
|
||||
|
||||
.. image:: fiscal_position/fiscal_position_02.png
|
||||
:align: center
|
||||
|
||||
Set up a default fiscal position
|
||||
================================
|
||||
|
||||
If you want to use a default fiscal position, meaning that a preexisting value is always
|
||||
automatically assigned, go to :menuselection:`Point of Sale --> Configuration
|
||||
--> Point of Sale` and enable *Fiscal Position*. Now, choose one to set as the default one.
|
||||
|
||||
.. image:: fiscal_position/fiscal_position_03.png
|
||||
:align: center
|
||||
.. image:: fiscal_position/set-tax.png
|
||||
:align: center
|
||||
|
||||
.. note::
|
||||
Now, the *tax* button is replaced by a *on site* button when on the *PoS* interface.
|
||||
If a default fiscal position is set, the tax button displays the name of the fiscal position.
|
||||
|
||||
.. seealso::
|
||||
:doc:`../../../finance/accounting/taxation/taxes/fiscal_positions`
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 6.8 KiB |
|
After Width: | Height: | Size: 4.5 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
|
@@ -9,6 +9,9 @@ How-to guides
|
|||
:titlesonly:
|
||||
|
||||
howtos/scss_tips
|
||||
howtos/javascript_field
|
||||
howtos/javascript_view
|
||||
howtos/javascript_client_action
|
||||
howtos/web_services
|
||||
howtos/company
|
||||
howtos/accounting_localization
|
||||
|
|
@@ -23,6 +26,21 @@ How-to guides
|
|||
|
||||
Follow this guide to keep the technical debt of your CSS code under control.
|
||||
|
||||
.. card:: Customize a field
|
||||
:target: howtos/javascript_field
|
||||
|
||||
Learn how to customize field components in the Odoo JavaScript web framework.
|
||||
|
||||
.. card:: Customize a view type
|
||||
:target: howtos/javascript_view
|
||||
|
||||
Learn how to customize view types in the Odoo JavaScript web framework.
|
||||
|
||||
.. card:: Create a client action
|
||||
:target: howtos/javascript_client_action
|
||||
|
||||
Learn how to create client actions in the Odoo JavaScript web framework.
|
||||
|
||||
.. card:: Web services
|
||||
:target: howtos/web_services
|
||||
|
||||
|
|
|
|||
46
content/developer/howtos/javascript_client_action.rst
Normal file
|
|
@@ -0,0 +1,46 @@
|
|||
|
||||
======================
|
||||
Create a client action
|
||||
======================
|
||||
|
||||
A client action triggers an action that is entirely implemented in the client side.
|
||||
One of the benefits of using a client action is the ability to create highly customized interfaces
|
||||
with ease. A client action is typically defined by an OWL component; we can also use the web
|
||||
framework and use services, core components, hooks,...
|
||||
|
||||
#. Create the :ref:`client action <reference/actions/client>`, don't forget to
|
||||
make it accessible.
|
||||
|
||||
.. code-block:: xml
|
||||
|
||||
<record model="ir.actions.client" id="my_client_action">
|
||||
<field name="name">My Client Action</field>
|
||||
<field name="tag">my_module.MyClientAction</field>
|
||||
</record>
|
||||
|
||||
#. Create a component that represents the client action.
|
||||
|
||||
.. code-block:: js
|
||||
:caption: :file:`my_client_action.js`
|
||||
|
||||
/** @odoo-module **/
|
||||
|
||||
import { registry } from "@web/core/registry";
|
||||
|
||||
import { Component } from "@odoo/owl";
|
||||
|
||||
class MyClientAction extends Component {}
|
||||
MyClientAction.template = "my_module.clientaction";
|
||||
|
||||
// remember the tag name we put in the first step
|
||||
registry.category("actions").add("my_module.MyClientAction", MyClientAction);
|
||||
|
||||
.. code-block:: xml
|
||||
:caption: :file:`my_client_action.xml`
|
||||
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<templates xml:space="preserve">
|
||||
<t t-name="awesome_tshirt.clientaction" owl="1">
|
||||
Hello world
|
||||
</t>
|
||||
</templates>
|
||||
107
content/developer/howtos/javascript_field.rst
Normal file
|
|
@@ -0,0 +1,107 @@
|
|||
|
||||
=================
|
||||
Customize a field
|
||||
=================
|
||||
|
||||
Subclass an existing field component
|
||||
====================================
|
||||
|
||||
Let's take an example where we want to extends the `BooleanField` to create a boolean field
|
||||
displaying "Late!" in red whenever the checkbox is checked.
|
||||
|
||||
#. Create a new widget component extending the desired field component.
|
||||
|
||||
.. code-block:: javascript
|
||||
:caption: :file:`late_order_boolean_field.js`
|
||||
|
||||
/** @odoo-module */
|
||||
|
||||
import { registry } from "@web/core/registry";
|
||||
import { BooleanField } from "@web/views/fields/boolean/boolean_field";
|
||||
import { Component, xml } from "@odoo/owl";
|
||||
|
||||
class LateOrderBooleanField extends BooleanField {}
|
||||
LateOrderBooleanField.template = "my_module.LateOrderBooleanField";
|
||||
|
||||
#. Create the field template.
|
||||
|
||||
The component uses a new template with the name `my_module.LateOrderBooleanField`. Create it by
|
||||
inheriting the current template of the `BooleanField`.
|
||||
|
||||
.. code-block:: xml
|
||||
:caption: :file:`late_order_boolean_field.xml`
|
||||
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<templates xml:space="preserve">
|
||||
<t t-name="my_module.LateOrderBooleanField" t-inherit="web.BooleanField" owl="1">
|
||||
<xpath expr="//CheckBox" position="after">
|
||||
<span t-if="props.value" class="text-danger"> Late! </span>
|
||||
</xpath>
|
||||
</t>
|
||||
</templates>
|
||||
|
||||
#. Register the component to the fields registry.
|
||||
|
||||
.. code-block::
|
||||
:caption: :file:`late_order_boolean_field.js`
|
||||
|
||||
registry.category("fields").add("late_boolean", LateOrderBooleanField);
|
||||
|
||||
#. Add the widget in the view arch as an attribute of the field.
|
||||
|
||||
.. code-block:: xml
|
||||
|
||||
<field name="somefield" widget="late_boolean"/>
|
||||
|
||||
Create a new field component
|
||||
============================
|
||||
|
||||
Assume that we want to create a field that displays a simple text in red.
|
||||
|
||||
#. Create a new Owl component representing our new field
|
||||
|
||||
.. code-block:: js
|
||||
:caption: :file:`my_text_field.js`
|
||||
|
||||
/** @odoo-module */
|
||||
|
||||
import { standardFieldProps } from "@web/views/fields/standard_field_props";
|
||||
import { Component, xml } from "@odoo/owl";
|
||||
import { registry } from "@web/core/registry";
|
||||
|
||||
export class MyTextField extends Component {
|
||||
|
||||
/**
|
||||
* @param {boolean} newValue
|
||||
*/
|
||||
onChange(newValue) {
|
||||
this.props.update(newValue);
|
||||
}
|
||||
}
|
||||
|
||||
MyTextField.template = xml`
|
||||
<input t-att-id="props.id" class="text-danger" t-att-value="props.value" onChange.bind="onChange" />
|
||||
`;
|
||||
MyTextField.props = {
|
||||
...standardFieldProps,
|
||||
};
|
||||
MyTextField.supportedTypes = ["char"];
|
||||
|
||||
The imported `standardFieldProps` contains the standard props passed by the `View` such as
|
||||
the `update` function to update the value, the `type` of the field in the model, the
|
||||
`readonly` boolean, and others.
|
||||
|
||||
#. In the same file, register the component to the fields registry.
|
||||
|
||||
.. code-block:: js
|
||||
:caption: :file:`my_text_field.js`
|
||||
|
||||
registry.category("fields").add("my_text_field", MyTextField);
|
||||
|
||||
This maps the widget name in the arch to its actual component.
|
||||
|
||||
#. Add the widget in the view arch as an attribute of the field.
|
||||
|
||||
.. code-block:: xml
|
||||
|
||||
<field name="somefield" widget="my_text_field"/>
|
||||
262
content/developer/howtos/javascript_view.rst
Normal file
|
|
@@ -0,0 +1,262 @@
|
|||
=====================
|
||||
Customize a view type
|
||||
=====================
|
||||
|
||||
Subclass an existing view
|
||||
=========================
|
||||
|
||||
Assume we need to create a custom version of a generic view. For example, a kanban view with some
|
||||
extra ribbon-like widget on top (to display some specific custom information). In that case, this
|
||||
can be done in a few steps:
|
||||
|
||||
#. Extend the kanban controller/renderer/model and register it in the view registry.
|
||||
|
||||
.. code-block:: js
|
||||
:caption: :file:`custom_kanban_controller.js`
|
||||
|
||||
/** @odoo-module */
|
||||
|
||||
import { KanbanController } from "@web/views/kanban/kanban_controller";
|
||||
import { kanbanView } from "@web/views/kanban/kanban_view";
|
||||
import { registry } from "@web/core/registry";
|
||||
|
||||
// the controller usually contains the Layout and the renderer.
|
||||
class CustomKanbanController extends KanbanController {
|
||||
// Your logic here, override or insert new methods...
|
||||
// if you override setup(), don't forget to call super.setup()
|
||||
}
|
||||
|
||||
CustomKanbanController.template = "my_module.CustomKanbanView";
|
||||
|
||||
export const customKanbanView = {
|
||||
...kanbanView, // contains the default Renderer/Controller/Model
|
||||
Controller: CustomKanbanController,
|
||||
};
|
||||
|
||||
// Register it to the views registry
|
||||
registry.category("views").add("custom_kanban", customeKanbanView);
|
||||
|
||||
In our custom kanban, we defined a new template. We can either inherit the kanban controller
|
||||
template and add our template pieces or we can define a completely new template.
|
||||
|
||||
.. code-block:: xml
|
||||
:caption: :file:`custom_kanban_controller.xml`
|
||||
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<templates>
|
||||
<t t-name="my_module.CustomKanbanView" t-inherit="web.KanbanView" owl="1">
|
||||
<xpath expr="//Layout" position="before">
|
||||
<div>
|
||||
Hello world !
|
||||
</div>
|
||||
</xpath>
|
||||
</t>
|
||||
</templates>
|
||||
|
||||
#. Use the view with the `js_class` attribute in arch.
|
||||
|
||||
.. code-block:: xml
|
||||
|
||||
<kanban js_class="custom_kanban">
|
||||
<templates>
|
||||
<t t-name="kanban-box">
|
||||
<!--Your comment-->
|
||||
</t>
|
||||
</templates>
|
||||
</kanban>
|
||||
|
||||
The possibilities for extending views are endless. While we have only extended the controller
|
||||
here, you can also extend the renderer to add new buttons, modify how records are presented, or
|
||||
customize the dropdown, as well as extend other components such as the model and `buttonTemplate`.
|
||||
|
||||
Create a new view from scratch
|
||||
==============================
|
||||
|
||||
Creating a new view is an advanced topic. This guide highlight only the essential steps.
|
||||
|
||||
#. Create the controller.
|
||||
|
||||
The primary role of a controller is to facilitate the coordination between various components
|
||||
of a view, such as the Renderer, Model, and Layout.
|
||||
|
||||
.. code-block:: js
|
||||
:caption: :file:`beautiful_controller.js`
|
||||
|
||||
/** @odoo-module */
|
||||
|
||||
import { Layout } from "@web/search/layout";
|
||||
import { useService } from "@web/core/utils/hooks";
|
||||
import { Component, onWillStart, useState} from "@odoo/owl";
|
||||
|
||||
export class BeautifulController extends Component {
|
||||
setup() {
|
||||
this.orm = useService("orm");
|
||||
|
||||
// The controller create the model and make it reactive so whenever this.model is
|
||||
// accessed and edited then it'll cause a rerendering
|
||||
this.model = useState(
|
||||
new this.props.Model(
|
||||
this.orm,
|
||||
this.props.resModel,
|
||||
this.props.fields,
|
||||
this.props.archInfo,
|
||||
this.props.domain
|
||||
)
|
||||
);
|
||||
|
||||
onWillStart(async () => {
|
||||
await this.model.load();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
BeautifulController.template = "my_module.View";
|
||||
BeautifulController.components = { Layout };
|
||||
|
||||
The template of the Controller displays the control panel with Layout and also the
|
||||
renderer.
|
||||
|
||||
.. code-block:: xml
|
||||
:caption: :file:`beautiful_controller.xml`
|
||||
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates xml:space="preserve">
|
||||
<t t-name="my_module.View" owl="1">
|
||||
<Layout display="props.display" className="'h-100 overflow-auto'">
|
||||
<t t-component="props.Renderer" records="model.records" propsYouWant="'Hello world'"/>
|
||||
</Layout>
|
||||
</t>
|
||||
</templates>
|
||||
|
||||
#. Create the renderer.
|
||||
|
||||
The primary function of a renderer is to generate a visual representation of data by rendering
|
||||
the view that includes records.
|
||||
|
||||
.. code-block:: js
|
||||
:caption: :file:`beautiful_renderer.js`
|
||||
|
||||
import { Component } from "@odoo/owl";
|
||||
export class BeautifulRenderer extends Component {}
|
||||
|
||||
BeautifulRenderer.template = "my_module.Renderer";
|
||||
|
||||
.. code-block:: xml
|
||||
:caption: :file:`beautiful_renderer.xml`
|
||||
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates xml:space="preserve">
|
||||
<t t-name="my_module.Renderer" owl="1">
|
||||
<t t-esc="props.propsYouWant"/>
|
||||
<t t-foreach="props.records" t-as="record" t-key="record.id">
|
||||
// Show records
|
||||
</t>
|
||||
</t>
|
||||
</templates>
|
||||
|
||||
#. Create the model.
|
||||
|
||||
The role of the model is to retrieve and manage all the necessary data in the view.
|
||||
|
||||
.. code-block:: js
|
||||
:caption: :file:`beautiful_model.js`
|
||||
|
||||
/** @odoo-module */
|
||||
|
||||
import { KeepLast } from "@web/core/utils/concurrency";
|
||||
|
||||
export class BeautifulModel {
|
||||
constructor(orm, resModel, fields, archInfo, domain) {
|
||||
this.orm = orm;
|
||||
this.resModel = resModel;
|
||||
// We can access arch information parsed by the beautiful arch parser
|
||||
const { fieldFromTheArch } = archInfo;
|
||||
this.fieldFromTheArch = fieldFromTheArch;
|
||||
this.fields = fields;
|
||||
this.domain = domain;
|
||||
this.keepLast = new KeepLast();
|
||||
}
|
||||
|
||||
async load() {
|
||||
// The keeplast protect against concurrency call
|
||||
const { length, records } = await this.keepLast.add(
|
||||
this.orm.webSearchRead(this.resModel, this.domain, [this.fieldsFromTheArch], {})
|
||||
);
|
||||
this.records = records;
|
||||
this.recordsLength = length;
|
||||
}
|
||||
}
|
||||
|
||||
.. note::
|
||||
|
||||
For advanced cases, instead of creating a model from scratch, it is also possible to use
|
||||
`RelationalModel`, which is used by other views.
|
||||
|
||||
#. Create the arch parser.
|
||||
|
||||
The role of the arch parser is to parse the arch view so the view has access to the information.
|
||||
|
||||
.. code-block:: js
|
||||
:caption: :file:`beautiful_arch_parser.js`
|
||||
|
||||
/** @odoo-module */
|
||||
|
||||
import { XMLParser } from "@web/core/utils/xml";
|
||||
|
||||
export class BeautifulArchParser extends XMLParser {
|
||||
parse(arch) {
|
||||
const xmlDoc = this.parseXML(arch);
|
||||
const fieldFromTheArch = xmlDoc.getAttribute("fieldFromTheArch");
|
||||
return {
|
||||
fieldFromTheArch,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
#. Create the view and combine all the pieces together, then register the view in the views
|
||||
registry.
|
||||
|
||||
.. code-block:: js
|
||||
:caption: :file:`beautiful_view.js`
|
||||
|
||||
/** @odoo-module */
|
||||
|
||||
import { registry } from "@web/core/registry";
|
||||
import { BeautifulController } from "./beautiful_controller";
|
||||
import { BeautifulArchParser } from "./beautiful_arch_parser";
|
||||
import { BeautifylModel } from "./beautiful_model";
|
||||
import { BeautifulRenderer } from "./beautiful_renderer";
|
||||
|
||||
export const beautifulView = {
|
||||
type: "beautiful",
|
||||
display_name: "Beautiful",
|
||||
icon: "fa fa-picture-o", // the icon that will be displayed in the Layout panel
|
||||
multiRecord: true,
|
||||
Controller: BeautifulController,
|
||||
ArchParser: BeautifulArchParser,
|
||||
Model: BeautifulModel,
|
||||
Renderer: BeautifulRenderer,
|
||||
|
||||
props(genericProps, view) {
|
||||
const { ArchParser } = view;
|
||||
const { arch } = genericProps;
|
||||
const archInfo = new ArchParser().parse(arch);
|
||||
|
||||
return {
|
||||
...genericProps,
|
||||
Model: view.Model,
|
||||
Renderer: view.Renderer,
|
||||
archInfo,
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
registry.category("views").add("beautifulView", beautifulView);
|
||||
|
||||
#. Use the view in an arch.
|
||||
|
||||
.. code-block:: xml
|
||||
|
||||
...
|
||||
<beautiful fieldFromTheArch="res.partner"/>
|
||||
...
|
||||
|
|
@@ -15,7 +15,6 @@ JavaScript framework
|
|||
frontend/services
|
||||
frontend/hooks
|
||||
frontend/patching_code
|
||||
frontend/javascript_cheatsheet
|
||||
frontend/javascript_reference
|
||||
frontend/mobile
|
||||
frontend/qweb
|
||||
|
|
|
|||
|
|
@@ -1,546 +0,0 @@
|
|||
|
||||
.. _reference/jscs:
|
||||
|
||||
=====================
|
||||
Javascript Cheatsheet
|
||||
=====================
|
||||
|
||||
There are many ways to solve a problem in JavaScript, and in Odoo. However, the
|
||||
Odoo framework was designed to be extensible (this is a pretty big constraint),
|
||||
and some common problems have a nice standard solution. The standard solution
|
||||
has probably the advantage of being easy to understand for an odoo developer,
|
||||
and will probably keep working when Odoo is modified.
|
||||
|
||||
This document tries to explain the way one could solve some of these issues.
|
||||
Note that this is not a reference. This is just a random collection of recipes,
|
||||
or explanations on how to proceed in some cases.
|
||||
|
||||
|
||||
First of all, remember that the first rule of customizing odoo with JS is:
|
||||
*try to do it in python*. This may seem strange, but the python framework is
|
||||
quite extensible, and many behaviours can be done simply with a touch of xml or
|
||||
python. This has usually a lower cost of maintenance than working with JS:
|
||||
|
||||
- the JS framework tends to change more, so JS code needs to be more frequently
|
||||
updated
|
||||
- it is often more difficult to implement a customized behaviour if it needs to
|
||||
communicate with the server and properly integrate with the javascript framework.
|
||||
There are many small details taken care by the framework that customized code
|
||||
needs to replicate. For example, responsiveness, or updating the url, or
|
||||
displaying data without flickering.
|
||||
|
||||
|
||||
.. note:: This document does not really explain any concepts. This is more a
|
||||
cookbook. For more details, please consult the javascript reference
|
||||
page (see :doc:`javascript_reference`)
|
||||
|
||||
Creating a new field widget
|
||||
===========================
|
||||
|
||||
This is probably a really common usecase: we want to display some information in
|
||||
a form view in a really specific (maybe business dependent) way. For example,
|
||||
assume that we want to change the text color depending on some business condition.
|
||||
|
||||
This can be done in three steps: creating a new widget, registering it in the
|
||||
field registry, then adding the widget to the field in the form view
|
||||
|
||||
- creating a new widget:
|
||||
This can be done by extending a widget:
|
||||
|
||||
.. code-block:: javascript
|
||||
|
||||
var FieldChar = require('web.basic_fields').FieldChar;
|
||||
|
||||
var CustomFieldChar = FieldChar.extend({
|
||||
_renderReadonly: function () {
|
||||
// implement some custom logic here
|
||||
},
|
||||
});
|
||||
|
||||
- registering it in the field registry:
|
||||
The web client needs to know the mapping between a widget name and its
|
||||
actual class. This is done by a registry:
|
||||
|
||||
.. code-block:: javascript
|
||||
|
||||
var fieldRegistry = require('web.field_registry');
|
||||
|
||||
fieldRegistry.add('my-custom-field', CustomFieldChar);
|
||||
|
||||
- adding the widget in the form view
|
||||
.. code-block:: xml
|
||||
|
||||
<field name="somefield" widget="my-custom-field"/>
|
||||
|
||||
Note that only the form, list and kanban views use this field widgets registry.
|
||||
These views are tightly integrated, because the list and kanban views can
|
||||
appear inside a form view).
|
||||
|
||||
Modifying an existing field widget
|
||||
==================================
|
||||
|
||||
Another use case is that we want to modify an existing field widget. For
|
||||
example, the voip addon in odoo need to modify the FieldPhone widget to add the
|
||||
possibility to easily call the given number on voip. This is done by *including*
|
||||
the FieldPhone widget, so there is no need to change any existing form view.
|
||||
|
||||
Field Widgets (instances of (subclass of) AbstractField) are like every other
|
||||
widgets, so they can be monkey patched. This looks like this:
|
||||
|
||||
.. code-block:: javascript
|
||||
|
||||
var basic_fields = require('web.basic_fields');
|
||||
var Phone = basic_fields.FieldPhone;
|
||||
|
||||
Phone.include({
|
||||
events: _.extend({}, Phone.prototype.events, {
|
||||
'click': '_onClick',
|
||||
}),
|
||||
|
||||
_onClick: function (e) {
|
||||
if (this.mode === 'readonly') {
|
||||
e.preventDefault();
|
||||
var phoneNumber = this.value;
|
||||
// call the number on voip...
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
Note that there is no need to add the widget to the registry, since it is already
|
||||
registered.
|
||||
|
||||
Modifying a main widget from the interface
|
||||
==========================================
|
||||
|
||||
Another common usecase is the need to customize some elements from the user
|
||||
interface. For example, adding a message in the home menu. The usual process
|
||||
in this case is again to *include* the widget. This is the only way to do it,
|
||||
since there are no registries for those widgets.
|
||||
|
||||
This is usually done with code looking like this:
|
||||
|
||||
.. code-block:: javascript
|
||||
|
||||
var HomeMenu = require('web_enterprise.HomeMenu');
|
||||
|
||||
HomeMenu.include({
|
||||
render: function () {
|
||||
this._super();
|
||||
// do something else here...
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
|
||||
Creating a new view (from scratch)
|
||||
==================================
|
||||
|
||||
Creating a new view is a more advanced topic. This cheatsheet will only
|
||||
highlight the steps that will probably need to be done (in no particular order):
|
||||
|
||||
- adding a new view type to the field ``type`` of ``ir.ui.view``::
|
||||
|
||||
class View(models.Model):
|
||||
_inherit = 'ir.ui.view'
|
||||
|
||||
type = fields.Selection(selection_add=[('map', "Map")])
|
||||
|
||||
- adding the new view type to the field ``view_mode`` of ``ir.actions.act_window.view``::
|
||||
|
||||
class ActWindowView(models.Model):
|
||||
_inherit = 'ir.actions.act_window.view'
|
||||
|
||||
view_mode = fields.Selection(selection_add=[('map', "Map")])
|
||||
|
||||
|
||||
- creating the four main pieces which makes a view (in JavaScript):
|
||||
we need a view (a subclass of ``AbstractView``, this is the factory), a
|
||||
renderer (from ``AbstractRenderer``), a controller (from ``AbstractController``)
|
||||
and a model (from ``AbstractModel``). I suggest starting by simply
|
||||
extending the superclasses:
|
||||
|
||||
.. code-block:: javascript
|
||||
|
||||
var AbstractController = require('web.AbstractController');
|
||||
var AbstractModel = require('web.AbstractModel');
|
||||
var AbstractRenderer = require('web.AbstractRenderer');
|
||||
var AbstractView = require('web.AbstractView');
|
||||
|
||||
var MapController = AbstractController.extend({});
|
||||
var MapRenderer = AbstractRenderer.extend({});
|
||||
var MapModel = AbstractModel.extend({});
|
||||
|
||||
var MapView = AbstractView.extend({
|
||||
config: {
|
||||
Model: MapModel,
|
||||
Controller: MapController,
|
||||
Renderer: MapRenderer,
|
||||
},
|
||||
});
|
||||
|
||||
- adding the view to the registry:
|
||||
As usual, the mapping between a view type and the actual class needs to be
|
||||
updated:
|
||||
|
||||
.. code-block:: javascript
|
||||
|
||||
var viewRegistry = require('web.view_registry');
|
||||
|
||||
viewRegistry.add('map', MapView);
|
||||
|
||||
- implementing the four main classes:
|
||||
The ``View`` class needs to parse the ``arch`` field and setup the other
|
||||
three classes. The ``Renderer`` is in charge of representing the data in
|
||||
the user interface, the ``Model`` is supposed to talk to the server, to
|
||||
load data and process it. And the ``Controller`` is there to coordinate,
|
||||
to talk to the web client, ...
|
||||
|
||||
- creating some views in the database:
|
||||
.. code-block:: xml
|
||||
|
||||
<record id="customer_map_view" model="ir.ui.view">
|
||||
<field name="name">customer.map.view</field>
|
||||
<field name="model">res.partner</field>
|
||||
<field name="arch" type="xml">
|
||||
<map latitude="partner_latitude" longitude="partner_longitude">
|
||||
<field name="name"/>
|
||||
</map>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
|
||||
Customizing an existing view
|
||||
============================
|
||||
|
||||
Assume we need to create a custom version of a generic view. For example, a
|
||||
kanban view with some extra *ribbon-like* widget on top (to display some
|
||||
specific custom information). In that case, this can be done with 3 steps:
|
||||
extend the kanban view (which also probably mean extending controllers/renderers
|
||||
and/or models), then registering the view in the view registry, and finally,
|
||||
using the view in the kanban arch (a specific example is the helpdesk dashboard).
|
||||
|
||||
- extending a view:
|
||||
Here is what it could look like:
|
||||
|
||||
.. code-block:: javascript
|
||||
|
||||
var HelpdeskDashboardRenderer = KanbanRenderer.extend({
|
||||
...
|
||||
});
|
||||
|
||||
var HelpdeskDashboardModel = KanbanModel.extend({
|
||||
...
|
||||
});
|
||||
|
||||
var HelpdeskDashboardController = KanbanController.extend({
|
||||
...
|
||||
});
|
||||
|
||||
var HelpdeskDashboardView = KanbanView.extend({
|
||||
config: _.extend({}, KanbanView.prototype.config, {
|
||||
Model: HelpdeskDashboardModel,
|
||||
Renderer: HelpdeskDashboardRenderer,
|
||||
Controller: HelpdeskDashboardController,
|
||||
}),
|
||||
});
|
||||
|
||||
- adding it to the view registry:
|
||||
as usual, we need to inform the web client of the mapping between the name
|
||||
of the views and the actual class.
|
||||
|
||||
.. code-block:: javascript
|
||||
|
||||
var viewRegistry = require('web.view_registry');
|
||||
viewRegistry.add('helpdesk_dashboard', HelpdeskDashboardView);
|
||||
|
||||
- using it in an actual view:
|
||||
we now need to inform the web client that a specific ``ir.ui.view`` needs to
|
||||
use our new class. Note that this is a web client specific concern. From
|
||||
the point of view of the server, we still have a kanban view. The proper
|
||||
way to do this is by using a special attribute ``js_class`` (which will be
|
||||
renamed someday into ``widget``, because this is really not a good name) on
|
||||
the root node of the arch:
|
||||
|
||||
.. code-block:: xml
|
||||
|
||||
<record id="helpdesk_team_view_kanban" model="ir.ui.view" >
|
||||
...
|
||||
<field name="arch" type="xml">
|
||||
<kanban js_class="helpdesk_dashboard">
|
||||
...
|
||||
</kanban>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
.. note::
|
||||
|
||||
Note: you can change the way the view interprets the arch structure. However,
|
||||
from the server point of view, this is still a view of the same base type,
|
||||
subjected to the same rules (rng validation, for example). So, your views still
|
||||
need to have a valid arch field.
|
||||
|
||||
Promises and asynchronous code
|
||||
==============================
|
||||
|
||||
For a very good and complete introduction to promises, please read this excellent article https://github.com/getify/You-Dont-Know-JS/blob/1st-ed/async%20%26%20performance/ch3.md
|
||||
|
||||
Creating new Promises
|
||||
---------------------
|
||||
|
||||
- turn a constant into a promise
|
||||
There are 2 static functions on Promise that create a resolved or rejected promise based on a constant:
|
||||
|
||||
.. code-block:: javascript
|
||||
|
||||
var p = Promise.resolve({blabla: '1'}); // creates a resolved promise
|
||||
p.then(function (result) {
|
||||
console.log(result); // --> {blabla: '1'};
|
||||
});
|
||||
|
||||
|
||||
var p2 = Promise.reject({error: 'error message'}); // creates a rejected promise
|
||||
p2.catch(function (reason) {
|
||||
console.log(reason); // --> {error: 'error message');
|
||||
});
|
||||
|
||||
|
||||
.. note:: Note that even if the promises are created already resolved or rejected, the `then` or `catch` handlers will still be called asynchronously.
|
||||
|
||||
|
||||
- based on an already asynchronous code
|
||||
Suppose that in a function you must do a rpc, and when it is completed set the result on this.
|
||||
The `this._rpc` is a function that returns a `Promise`.
|
||||
|
||||
.. code-block:: javascript
|
||||
|
||||
function callRpc() {
|
||||
var self = this;
|
||||
return this._rpc(...).then(function (result) {
|
||||
self.myValueFromRpc = result;
|
||||
});
|
||||
}
|
||||
|
||||
- for callback based function
|
||||
Suppose that you were using a function `this.close` that takes as parameter a callback that is called when the closing is finished.
|
||||
Now suppose that you are doing that in a method that must send a promise that is resolved when the closing is finished.
|
||||
|
||||
.. code-block:: javascript
|
||||
:linenos:
|
||||
|
||||
function waitForClose() {
|
||||
var self = this;
|
||||
return new Promise (function(resolve, reject) {
|
||||
self.close(resolve);
|
||||
});
|
||||
}
|
||||
|
||||
* line 2: we save the `this` into a variable so that in an inner function, we can access the scope of our component
|
||||
* line 3: we create and return a new promise. The constructor of a promise takes a function as parameter. This function itself has 2 parameters that we called here `resolve` and `reject`
|
||||
- `resolve` is a function that, when called, puts the promise in the resolved state.
|
||||
- `reject` is a function that, when called, puts the promise in the rejected state. We do not use reject here and it can be omitted.
|
||||
* line 4: we are calling the function close on our object. It takes a function as parameter (the callback) and it happens that resolve is already a function, so we can pass it directly. To be clearer, we could have written:
|
||||
|
||||
.. code-block:: javascript
|
||||
|
||||
return new Promise (function (resolve) {
|
||||
self.close(function () {
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
|
||||
- creating a promise generator (calling one promise after the other *in sequence* and waiting for the last one)
|
||||
Suppose that you need to loop over an array, do an operation *in sequence* and resolve a promise when the last operation is done.
|
||||
|
||||
.. code-block:: javascript
|
||||
|
||||
function doStuffOnArray(arr) {
|
||||
var done = Promise.resolve();
|
||||
arr.forEach(function (item) {
|
||||
done = done.then(function () {
|
||||
return item.doSomethingAsynchronous();
|
||||
});
|
||||
});
|
||||
return done;
|
||||
}
|
||||
|
||||
This way, the promise you return is effectively the last promise.
|
||||
- creating a promise, then resolving it outside the scope of its definition (anti-pattern)
|
||||
.. note:: we do not recommend using this, but sometimes it is useful. Think carefully for alternatives first...
|
||||
|
||||
.. code-block:: javascript
|
||||
|
||||
...
|
||||
var resolver, rejecter;
|
||||
var prom = new Promise(function (resolve, reject){
|
||||
resolver = resolve;
|
||||
rejecter = reject;
|
||||
});
|
||||
...
|
||||
|
||||
resolver("done"); // will resolve the promise prom with the result "done"
|
||||
rejecter("error"); // will reject the promise prom with the reason "error"
|
||||
|
||||
Waiting for Promises
|
||||
--------------------
|
||||
|
||||
- waiting for a number of Promises
|
||||
if you have multiple promises that all need to be waited, you can convert them into a single promise that will be resolved when all the promises are resolved using Promise.all(arrayOfPromises).
|
||||
|
||||
.. code-block:: javascript
|
||||
|
||||
var prom1 = doSomethingThatReturnsAPromise();
|
||||
var prom2 = Promise.resolve(true);
|
||||
var constant = true;
|
||||
|
||||
var all = Promise.all([prom1, prom2, constant]); // all is a promise
|
||||
// results is an array, the individual results correspond to the index of their
|
||||
// promise as called in Promise.all()
|
||||
all.then(function (results) {
|
||||
var prom1Result = results[0];
|
||||
var prom2Result = results[1];
|
||||
var constantResult = results[2];
|
||||
});
|
||||
return all;
|
||||
|
||||
|
||||
- waiting for a part of a promise chain, but not another part
|
||||
If you have an asynchronous process that you want to wait to do something, but you also want to return to the caller before that something is done.
|
||||
|
||||
.. code-block:: javascript
|
||||
|
||||
function returnAsSoonAsAsyncProcessIsDone() {
|
||||
var prom = AsyncProcess();
|
||||
prom.then(function (resultOfAsyncProcess) {
|
||||
return doSomething();
|
||||
});
|
||||
/* returns prom which will only wait for AsyncProcess(),
|
||||
and when it will be resolved, the result will be the one of AsyncProcess */
|
||||
return prom;
|
||||
}
|
||||
|
||||
Error handling
|
||||
--------------
|
||||
|
||||
- in general in promises
|
||||
The general idea is that a promise should not be rejected for control flow, but should only be rejected for errors.
|
||||
When that is the case, you would have multiple resolutions of your promise with, for instance status codes that you would have to check in the `then` handlers and a single `catch` handler at the end of the promise chain.
|
||||
|
||||
.. code-block:: javascript
|
||||
|
||||
function a() {
|
||||
x.y(); // <-- this is an error: x is undefined
|
||||
return Promise.resolve(1);
|
||||
}
|
||||
function b() {
|
||||
return Promise.reject(2);
|
||||
}
|
||||
|
||||
a().catch(console.log); // will log the error in a
|
||||
a().then(b).catch(console.log); // will log the error in a, the then is not executed
|
||||
b().catch(console.log); // will log the rejected reason of b (2)
|
||||
Promise.resolve(1)
|
||||
.then(b) // the then is executed, it executes b
|
||||
.then(...) // this then is not executed
|
||||
.catch(console.log); // will log the rejected reason of b (2)
|
||||
|
||||
|
||||
|
||||
- in Odoo specifically
|
||||
In Odoo, it happens that we use promise rejection for control flow, like in mutexes and other concurrency primitives defined in module `web.concurrency`
|
||||
We also want to execute the catch for *business* reasons, but not when there is a coding error in the definition of the promise or of the handlers.
|
||||
For this, we have introduced the concept of `guardedCatch`. It is called like `catch` but not when the rejected reason is an error
|
||||
|
||||
.. code-block:: javascript
|
||||
|
||||
function blabla() {
|
||||
if (someCondition) {
|
||||
return Promise.reject("someCondition is truthy");
|
||||
}
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
// ...
|
||||
|
||||
var promise = blabla();
|
||||
promise.then(function (result) { console.log("everything went fine"); })
|
||||
// this will be called if blabla returns a rejected promise, but not if it has an error
|
||||
promise.guardedCatch(function (reason) { console.log(reason); });
|
||||
|
||||
// ...
|
||||
|
||||
var anotherPromise =
|
||||
blabla().then(function () { console.log("everything went fine"); })
|
||||
// this will be called if blabla returns a rejected promise,
|
||||
// but not if it has an error
|
||||
.guardedCatch(console.log);
|
||||
|
||||
|
||||
.. code-block:: javascript
|
||||
|
||||
var promiseWithError = Promise.resolve().then(function () {
|
||||
x.y(); // <-- this is an error: x is undefined
|
||||
});
|
||||
promiseWithError.guardedCatch(function (reason) {console.log(reason);}); // will not be called
|
||||
promiseWithError.catch(function (reason) {console.log(reason);}); // will be called
|
||||
|
||||
|
||||
|
||||
Testing asynchronous code
|
||||
-------------------------
|
||||
|
||||
- using promises in tests
|
||||
In the tests code, we support the latest version of Javascript, including primitives like `async` and `await`. This makes using and waiting for promises very easy.
|
||||
Most helper methods also return a promise (either by being marked `async` or by returning a promise directly.
|
||||
|
||||
.. code-block:: javascript
|
||||
|
||||
var testUtils = require('web.test_utils');
|
||||
QUnit.test("My test", async function (assert) {
|
||||
// making the function async has 2 advantages:
|
||||
// 1) it always returns a promise so you don't need to define `var done = assert.async()`
|
||||
// 2) it allows you to use the `await`
|
||||
assert.expect(1);
|
||||
|
||||
var form = await testUtils.createView({ ... });
|
||||
await testUtils.form.clickEdit(form);
|
||||
await testUtils.form.click('jquery selector');
|
||||
assert.containsOnce('jquery selector');
|
||||
form.destroy();
|
||||
});
|
||||
|
||||
QUnit.test("My test - no async - no done", function (assert) {
|
||||
// this function is not async, but it returns a promise.
|
||||
// QUnit will wait for for this promise to be resolved.
|
||||
assert.expect(1);
|
||||
|
||||
return testUtils.createView({ ... }).then(function (form) {
|
||||
return testUtils.form.clickEdit(form).then(function () {
|
||||
return testUtils.form.click('jquery selector').then(function () {
|
||||
assert.containsOnce('jquery selector');
|
||||
form.destroy();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
QUnit.test("My test - no async", function (assert) {
|
||||
// this function is not async and does not return a promise.
|
||||
// we have to use the done function to signal QUnit that the test is async and will be finished inside an async callback
|
||||
assert.expect(1);
|
||||
var done = assert.async();
|
||||
|
||||
testUtils.createView({ ... }).then(function (form) {
|
||||
testUtils.form.clickEdit(form).then(function () {
|
||||
testUtils.form.click('jquery selector').then(function () {
|
||||
assert.containsOnce('jquery selector');
|
||||
form.destroy();
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
as you can see, the nicer form is to use `async/await` as it is clearer and shorter to write.
|
||||
|
|
@@ -288,13 +288,6 @@ The final touch is to let the user delete a todo.
|
|||
Owl has a powerful `slot <{OWL_PATH}/doc/reference/slots.md>`_ system to allow you to write generic
|
||||
components. This is useful to factorize the common layout between different parts of the interface.
|
||||
|
||||
Imagine that you want to display some cards with each the title and a description of a film.
|
||||
Clearly, the parent component needs to *define* a content for each card, and each card need
|
||||
to *insert* the content given by the parent component. This is done by using the directives
|
||||
`t-set-slot="slotname"` (to define) and `t-slot="slotname"` (to insert).
|
||||
|
||||
.. tip:: The value of the slot has to be inside the slot tag. Don't forget that t-slot="SlotName"
|
||||
|
||||
.. exercise::
|
||||
|
||||
#. Write a `Card` component using the following Bootstrap HTML structure:
|
||||
|
|
|
|||
|
|
@@ -46,4 +46,4 @@ developer/howtos/discover_js_framework/07_testing.rst developer/tutorials/discov
|
|||
# developer/reference/frontend
|
||||
|
||||
developer/reference/frontend/icons_library.rst contributing/development/ui/icons.rst # Odoo UI icons -> UI Icons
|
||||
|
||||
developer/reference/frontend/javascript_cheatsheet.rst developer/howtos/javascript_create_field.rst # refactor JavaScript cheatsheet into howtos
|
||||
|
|
|
|||