From 0a12ce4138ba0628e3ac0a4abcd2fd26e501f939 Mon Sep 17 00:00:00 2001
From: Tom Blauwendraat
Date: Thu, 28 Jan 2021 10:04:06 +0100
Subject: [PATCH 01/16] [ADD] sequence_python
---
sequence_python/README.rst | 113 +++++
sequence_python/__init__.py | 1 +
sequence_python/__manifest__.py | 13 +
sequence_python/i18n/sequence_python.pot | 108 +++++
sequence_python/models/__init__.py | 1 +
sequence_python/models/ir_sequence.py | 78 +++
sequence_python/readme/CONTRIBUTORS.rst | 1 +
sequence_python/readme/DESCRIPTION.rst | 16 +
sequence_python/readme/USAGE.rst | 21 +
sequence_python/static/description/icon.png | Bin 0 -> 9455 bytes
sequence_python/static/description/index.html | 455 ++++++++++++++++++
sequence_python/tests/__init__.py | 3 +
sequence_python/tests/test_ir_sequence.py | 39 ++
sequence_python/views/ir_sequence.xml | 52 ++
14 files changed, 901 insertions(+)
create mode 100644 sequence_python/README.rst
create mode 100644 sequence_python/__init__.py
create mode 100644 sequence_python/__manifest__.py
create mode 100644 sequence_python/i18n/sequence_python.pot
create mode 100644 sequence_python/models/__init__.py
create mode 100644 sequence_python/models/ir_sequence.py
create mode 100644 sequence_python/readme/CONTRIBUTORS.rst
create mode 100644 sequence_python/readme/DESCRIPTION.rst
create mode 100644 sequence_python/readme/USAGE.rst
create mode 100644 sequence_python/static/description/icon.png
create mode 100644 sequence_python/static/description/index.html
create mode 100644 sequence_python/tests/__init__.py
create mode 100644 sequence_python/tests/test_ir_sequence.py
create mode 100644 sequence_python/views/ir_sequence.xml
diff --git a/sequence_python/README.rst b/sequence_python/README.rst
new file mode 100644
index 00000000000..51a1cdbd471
--- /dev/null
+++ b/sequence_python/README.rst
@@ -0,0 +1,113 @@
+===============================
+Sequence from Python expression
+===============================
+
+.. !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
+ !! This file is generated by oca-gen-addon-readme !!
+ !! changes will be overwritten. !!
+ !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
+
+.. |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%2Fserver--tools-lightgray.png?logo=github
+ :target: https://github.com/OCA/server-tools/tree/13.0/sequence_python
+ :alt: OCA/server-tools
+.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png
+ :target: https://translation.odoo-community.org/projects/server-tools-13-0/server-tools-13-0-sequence_python
+ :alt: Translate me on Weblate
+.. |badge5| image:: https://img.shields.io/badge/runbot-Try%20me-875A7B.png
+ :target: https://runbot.odoo-community.org/runbot/149/13.0
+ :alt: Try me on Runbot
+
+|badge1| |badge2| |badge3| |badge4| |badge5|
+
+This module allows to generate a sequence by a Python formula expression.
+
+Besides common Python functions and operators, it provides several functions
+like 'random' and 'uuid' in the expression, as well as variables such as the
+next number in the sequence. These are also listed next to the input field
+on the sequence form view.
+
+If you want to add more variables for use in the expression, you can extend
+this module.
+
+Use cases for this module could be:
+
+- You want to generate alphanumeric numbering
+- You want to apply some math to the number to prevent customers from knowing
+ their place in the sequence
+- You want to use UUID
+
+**Table of contents**
+
+.. contents::
+ :local:
+
+Usage
+=====
+
+To use this module, you need to:
+
+* Go to the form view of an `ir.sequence` record
+* Go to the Python tab
+* Enable the 'Use Python' checkbox
+* Change the default 'number' expression to something more fancy.
+
+Examples:
+
+.. code-block:: python
+
+ # To separate the Odoo-generated number with hyphens eg. 0-0-0-0-1
+ '-'.join(number_padded)
+
+ # To have an UUID as the sequence value
+ uuid.uuid4().hex
+
+ # To use an 8-digit binary number
+ '{0:#010b}'.format(number + 300)[2:]
+
+And so on.
+
+Bug Tracker
+===========
+
+Bugs are tracked on `GitHub Issues `_.
+In case of trouble, please check there if your issue has already been reported.
+If you spotted it first, help us smashing it by providing a detailed and welcomed
+`feedback `_.
+
+Do not contact contributors directly about support or help with technical issues.
+
+Credits
+=======
+
+Authors
+~~~~~~~
+
+* Sunflower IT
+
+Contributors
+~~~~~~~~~~~~
+
+* Tom Blauwendraat
+
+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/server-tools `_ project on GitHub.
+
+You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.
diff --git a/sequence_python/__init__.py b/sequence_python/__init__.py
new file mode 100644
index 00000000000..0650744f6bc
--- /dev/null
+++ b/sequence_python/__init__.py
@@ -0,0 +1 @@
+from . import models
diff --git a/sequence_python/__manifest__.py b/sequence_python/__manifest__.py
new file mode 100644
index 00000000000..9b6b1eeb83e
--- /dev/null
+++ b/sequence_python/__manifest__.py
@@ -0,0 +1,13 @@
+# Copyright 2020 ACSONE SA/NV ()
+# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
+{
+ "name": "Sequence from Python expression",
+ "summary": """Calculate a sequence number from a Python expression""",
+ "author": "Sunflower IT,Odoo Community Association (OCA)",
+ "website": "https://github.com/OCA/server-tools",
+ "version": "13.0.1.0.0",
+ "license": "AGPL-3",
+ "category": "Generic Modules",
+ "depends": ["base"],
+ "data": ["views/ir_sequence.xml"],
+}
diff --git a/sequence_python/i18n/sequence_python.pot b/sequence_python/i18n/sequence_python.pot
new file mode 100644
index 00000000000..87cb2ef655e
--- /dev/null
+++ b/sequence_python/i18n/sequence_python.pot
@@ -0,0 +1,108 @@
+# Translation of Odoo Server.
+# This file contains the translation of the following modules:
+# * sequence_python
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: Odoo Server 13.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: sequence_python
+#: model_terms:ir.ui.view,arch_db:sequence_python.sequence_view
+msgid "number: The next number of the sequence (integer)"
+msgstr ""
+
+#. module: sequence_python
+#: model_terms:ir.ui.view,arch_db:sequence_python.sequence_view
+msgid ""
+"number_padded: Padded string of the next number of the sequence"
+msgstr ""
+
+#. module: sequence_python
+#: model_terms:ir.ui.view,arch_db:sequence_python.sequence_view
+msgid ""
+"random: The Python random module, eg. to use "
+"random.randint(0, 9)"
+msgstr ""
+
+#. module: sequence_python
+#: model_terms:ir.ui.view,arch_db:sequence_python.sequence_view
+msgid "sequence: Odoo record of the sequence being used"
+msgstr ""
+
+#. module: sequence_python
+#: model_terms:ir.ui.view,arch_db:sequence_python.sequence_view
+msgid ""
+"string: The Python string module, eg. to use "
+"random.choices(string.ascii_letters + string.digits, k=4)"
+msgstr ""
+
+#. module: sequence_python
+#: model_terms:ir.ui.view,arch_db:sequence_python.sequence_view
+msgid ""
+"uuid: The Python uuid module, eg. to use "
+"uuid.uuid4()"
+msgstr ""
+
+#. module: sequence_python
+#: model_terms:ir.ui.view,arch_db:sequence_python.sequence_view
+msgid "Aside from this, you may use several"
+msgstr ""
+
+#. module: sequence_python
+#: model_terms:ir.ui.view,arch_db:sequence_python.sequence_view
+msgid "Help with Python expressions"
+msgstr ""
+
+#. module: sequence_python
+#: model:ir.model.fields,field_description:sequence_python.field_ir_sequence__python_code_preview
+msgid "Preview"
+msgstr ""
+
+#. module: sequence_python
+#: model_terms:ir.ui.view,arch_db:sequence_python.sequence_view
+msgid "Python Code"
+msgstr ""
+
+#. module: sequence_python
+#: model:ir.model.fields,field_description:sequence_python.field_ir_sequence__python_code
+msgid "Python expression"
+msgstr ""
+
+#. module: sequence_python
+#: model:ir.model,name:sequence_python.model_ir_sequence
+msgid "Sequence"
+msgstr ""
+
+#. module: sequence_python
+#: model_terms:ir.ui.view,arch_db:sequence_python.sequence_view
+msgid ""
+"The expression you type here will be evaluated as the next number. The "
+"following variables can be used:"
+msgstr ""
+
+#. module: sequence_python
+#: model:ir.model.fields,field_description:sequence_python.field_ir_sequence__use_python_code
+msgid "Use Python"
+msgstr ""
+
+#. module: sequence_python
+#: model:ir.model.fields,help:sequence_python.field_ir_sequence__python_code
+msgid "Write Python code that generates the sequence body."
+msgstr ""
+
+#. module: sequence_python
+#: model_terms:ir.ui.view,arch_db:sequence_python.sequence_view
+msgid "builtin Python functions"
+msgstr ""
+
+#. module: sequence_python
+#: model_terms:ir.ui.view,arch_db:sequence_python.sequence_view
+msgid "number"
+msgstr ""
diff --git a/sequence_python/models/__init__.py b/sequence_python/models/__init__.py
new file mode 100644
index 00000000000..5b015772ab2
--- /dev/null
+++ b/sequence_python/models/__init__.py
@@ -0,0 +1 @@
+from . import ir_sequence
diff --git a/sequence_python/models/ir_sequence.py b/sequence_python/models/ir_sequence.py
new file mode 100644
index 00000000000..7c1535e7b00
--- /dev/null
+++ b/sequence_python/models/ir_sequence.py
@@ -0,0 +1,78 @@
+# Copyright 2020 Sunflower IT ()
+# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
+import logging
+import random
+import string
+import uuid
+
+from odoo import fields, models
+from odoo.tools import safe_eval
+
+_logger = logging.getLogger(__name__)
+
+
+DEFAULT_PYTHON_CODE = "number_padded"
+
+
+class IrSequence(models.Model):
+ """
+ Inherit standard ir.sequence to let the possibility of using a Python formula
+ to calculate the sequence from input variables such as the sequence number.
+ This allows obfuscation of the order in which sequences are given out,
+ but can also be used for any other purpose.
+ """
+
+ _inherit = "ir.sequence"
+
+ # Python code
+ use_python_code = fields.Boolean(string="Use Python", default=False)
+ python_code = fields.Text(
+ string="Python expression",
+ default=DEFAULT_PYTHON_CODE,
+ help="Write Python code that generates the sequence body.",
+ )
+ python_code_preview = fields.Char("Preview", compute="_compute_python_code_preview")
+
+ def _get_python_eval_context(self, number_next):
+ """
+ Get the eval context to evaluate the Python code with.
+ The format is (variable name, description, value)
+ You can inherit this in your custom module.
+ :return: tuple
+ """
+ return {
+ "number": number_next[0] if isinstance(number_next, tuple) else number_next,
+ "number_padded": "%%0%sd" % self.padding % number_next,
+ "sequence": self,
+ "random": random,
+ "uuid": uuid,
+ "string": string,
+ }
+
+ def _get_python_value(self, number_next):
+ """
+ Use the python formula to get the value.
+ :return: string
+ """
+ eval_context = self._get_python_eval_context(number_next)
+ return safe_eval(self.python_code.strip(), eval_context)
+
+ def _compute_python_code_preview(self):
+ for this in self:
+ try:
+ this.python_code_preview = self.get_next_char(
+ (self.number_next_actual,)
+ )
+ except Exception as e: # noqa
+ this.python_code_preview = str(e)
+
+ def get_next_char(self, number_next):
+ if self.use_python_code:
+ interpolated_prefix, interpolated_suffix = self._get_prefix_suffix()
+ return (
+ interpolated_prefix
+ + self._get_python_value(number_next)
+ + interpolated_suffix
+ )
+ else:
+ return super(IrSequence, self).get_next_char(number_next)
diff --git a/sequence_python/readme/CONTRIBUTORS.rst b/sequence_python/readme/CONTRIBUTORS.rst
new file mode 100644
index 00000000000..68c0876aa4b
--- /dev/null
+++ b/sequence_python/readme/CONTRIBUTORS.rst
@@ -0,0 +1 @@
+* Tom Blauwendraat
diff --git a/sequence_python/readme/DESCRIPTION.rst b/sequence_python/readme/DESCRIPTION.rst
new file mode 100644
index 00000000000..8dc3416572e
--- /dev/null
+++ b/sequence_python/readme/DESCRIPTION.rst
@@ -0,0 +1,16 @@
+This module allows to generate a sequence by a Python formula expression.
+
+Besides common Python functions and operators, it provides several functions
+like 'random' and 'uuid' in the expression, as well as variables such as the
+next number in the sequence. These are also listed next to the input field
+on the sequence form view.
+
+If you want to add more variables for use in the expression, you can extend
+this module.
+
+Use cases for this module could be:
+
+- You want to generate alphanumeric numbering
+- You want to apply some math to the number to prevent customers from knowing
+ their place in the sequence
+- You want to use UUID
diff --git a/sequence_python/readme/USAGE.rst b/sequence_python/readme/USAGE.rst
new file mode 100644
index 00000000000..67deb469116
--- /dev/null
+++ b/sequence_python/readme/USAGE.rst
@@ -0,0 +1,21 @@
+To use this module, you need to:
+
+* Go to the form view of an `ir.sequence` record
+* Go to the Python tab
+* Enable the 'Use Python' checkbox
+* Change the default 'number' expression to something more fancy.
+
+Examples:
+
+.. code-block:: python
+
+ # To separate the Odoo-generated number with hyphens eg. 0-0-0-0-1
+ '-'.join(number_padded)
+
+ # To have an UUID as the sequence value
+ uuid.uuid4().hex
+
+ # To use an 8-digit binary number
+ '{0:#010b}'.format(number + 300)[2:]
+
+And so on.
diff --git a/sequence_python/static/description/icon.png b/sequence_python/static/description/icon.png
new file mode 100644
index 0000000000000000000000000000000000000000..3a0328b516c4980e8e44cdb63fd945757ddd132d
GIT binary patch
literal 9455
zcmW++2RxMjAAjx~&dlBk9S+%}OXg)AGE&Cb*&}d0jUxM@u(PQx^-s)697TX`ehR4?GS^qbkof1cslKgkU)h65qZ9Oc=ml_0temigYLJfnz{IDzUf>bGs4N!v3=Z3jMq&A#7%rM5eQ#dc?k~!
zVpnB`o+K7|Al`Q_U;eD$B
zfJtP*jH`siUq~{KE)`jP2|#TUEFGRryE2`i0**z#*^6~AI|YzIWy$Cu#CSLW3q=GA
z6`?GZymC;dCPk~rBS%eCb`5OLr;RUZ;D`}um=H)BfVIq%7VhiMr)_#G0N#zrNH|__
zc+blN2UAB0=617@>_u;MPHN;P;N#YoE=)R#i$k_`UAA>WWCcEVMh~L_
zj--gtp&|K1#58Yz*AHCTMziU1Jzt_jG0I@qAOHsk$2}yTmVkBp_eHuY$A9)>P6o~I
z%aQ?!(GqeQ-Y+b0I(m9pwgi(IIZZzsbMv+9w{PFtd_<_(LA~0H(xz{=FhLB@(1&qHA5EJw1>>=%q2f&^X>IQ{!GJ4e9U
z&KlB)z(84HmNgm2hg2C0>WM{E(DdPr+EeU_N@57;PC2&DmGFW_9kP&%?X4}+xWi)(
z;)z%wI5>D4a*5XwD)P--sPkoY(a~WBw;E~AW`Yue4kFa^LM3X`8x|}ZUeMnqr}>kH
zG%WWW>3ml$Yez?i%)2pbKPI7?5o?hydokgQyZsNEr{a|mLdt;X2TX(#B1j35xPnPW
z*bMSSOauW>o;*=kO8ojw91VX!qoOQb)zHJ!odWB}d+*K?#sY_jqPdg{Sm2HdYzdEx
zOGVPhVRTGPtv0o}RfVP;Nd(|CB)I;*t&QO8h
zFfekr30S!-LHmV_Su-W+rEwYXJ^;6&3|L$mMC8*bQptyOo9;>Qb9Q9`ySe3%V$A*9
zeKEe+b0{#KWGp$F+tga)0RtI)nhMa-K@JS}2krK~n8vJ=Ngm?R!9G<~RyuU0d?nz#
z-5EK$o(!F?hmX*2Yt6+coY`6jGbb7tF#6nHA
zuKk=GGJ;ZwON1iAfG$E#Y7MnZVmrY|j0eVI(DN_MNFJmyZ|;w4tf@=CCDZ#5N_0K=
z$;R~bbk?}TpfDjfB&aiQ$VA}s?P}xPERJG{kxk5~R`iRS(SK5d+Xs9swCozZISbnS
zk!)I0>t=A<-^z(cmSFz3=jZ23u13X><0b)P)^1T_))Kr`e!-pb#q&J*Q`p+B6la%C
zuVl&0duN<;uOsB3%T9Fp8t{ED108)`y_~Hnd9AUX7h-H?jVuU|}My+C=TjH(jKz
zqMVr0re3S$H@t{zI95qa)+Crz*5Zj}Ao%4Z><+W(nOZd?gDnfNBC3>M8WE61$So|P
zVvqH0SNtDTcsUdzaMDpT=Ty0pDHHNL@Z0w$Y`XO
z2M-_r1S+GaH%pz#Uy0*w$Vdl=X=rQXEzO}d6J^R6zjM1u&c9vYLvLp?W7w(?np9x1
zE_0JSAJCPB%i7p*Wvg)pn5T`8k3-uR?*NT|J`eS#_#54p>!p(mLDvmc-3o0mX*mp_
zN*AeS<>#^-{S%W<*mz^!X$w_2dHWpcJ6^j64qFBft-o}o_Vx80o0>}Du;>kLts;$8
zC`7q$QI(dKYG`Wa8#wl@V4jVWBRGQ@1dr-hstpQL)Tl+aqVpGpbSfN>5i&QMXfiZ>
zaA?T1VGe?rpQ@;+pkrVdd{klI&jVS@I5_iz!=UMpTsa~mBga?1r}aRBm1WS;TT*s0f0lY=JBl66Upy)-k4J}lh=P^8(SXk~0xW=T9v*B|gzIhN
z>qsO7dFd~mgxAy4V?&)=5ieYq?zi?ZEoj)&2o)RLy=@hbCRcfT5jigwtQGE{L*8<@Yd{zg;CsL5mvzfDY}P-wos_6PfprFVaeqNE%h
zKZhLtcQld;ZD+>=nqN~>GvROfueSzJD&BE*}XfU|H&(FssBqY=hPCt`d
zH?@s2>I(|;fcW&YM6#V#!kUIP8$Nkdh0A(bEVj``-AAyYgwY~jB
zT|I7Bf@%;7aL7Wf4dZ%VqF$eiaC38OV6oy3Z#TER2G+fOCd9Iaoy6aLYbPTN{XRPz
z;U!V|vBf%H!}52L2gH_+j;`bTcQRXB+y9onc^wLm5wi3-Be}U>k_u>2Eg$=k!(l@I
zcCg+flakT2Nej3i0yn+g+}%NYb?ta;R?(g5SnwsQ49U8Wng8d|{B+lyRcEDvR3+`O{zfmrmvFrL6acVP%yG98X
zo&+VBg@px@i)%o?dG(`T;n*$S5*rnyiR#=wW}}GsAcfyQpE|>a{=$Hjg=-*_K;UtD
z#z-)AXwSRY?OPefw^iI+
z)AXz#PfEjlwTes|_{sB?4(O@fg0AJ^g8gP}ex9Ucf*@_^J(s_5jJV}c)s$`Myn|Kd
z$6>}#q^n{4vN@+Os$m7KV+`}c%4)4pv@06af4-x5#wj!KKb%caK{A&Y#Rfs
z-po?Dcb1({W=6FKIUirH&(yg=*6aLCekcKwyfK^JN5{wcA3nhO(o}SK#!CINhI`-I
z1)6&n7O&ZmyFMuNwvEic#IiOAwNkR=u5it{B9n2sAJV5pNhar=j5`*N!Na;c7g!l$
z3aYBqUkqqTJ=Re-;)s!EOeij=7SQZ3Hq}ZRds%IM*PtM$wV
z@;rlc*NRK7i3y5BETSKuumEN`Xu_8GP1Ri=OKQ$@I^ko8>H6)4rjiG5{VBM>B|%`&&s^)jS|-_95&yc=GqjNo{zFkw%%HHhS~e=s
zD#sfS+-?*t|J!+ozP6KvtOl!R)@@-z24}`9{QaVLD^9VCSR2b`b!KC#o;Ki<+wXB6
zx3&O0LOWcg4&rv4QG0)4yb}7BFSEg~=IR5#ZRj8kg}dS7_V&^%#Do==#`u
zpy6{ox?jWuR(;pg+f@mT>#HGWHAJRRDDDv~@(IDw&R>9643kK#HN`!1vBJHnC+RM&yIh8{gG2q
zA%e*U3|N0XSRa~oX-3EAneep)@{h2vvd3Xvy$7og(sayr@95+e6~Xvi1tUqnIxoIH
zVWo*OwYElb#uyW{Imam6f2rGbjR!Y3`#gPqkv57dB6K^wRGxc9B(t|aYDGS=m$&S!NmCtrMMaUg(c
zc2qC=2Z`EEFMW-me5B)24AqF*bV5Dr-M5ig(l-WPS%CgaPzs6p_gnCIvTJ=Y<6!gT
zVt@AfYCzjjsMEGi=rDQHo0yc;HqoRNnNFeWZgcm?f;cp(6CNylj36DoL(?TS7eU#+
z7&mfr#y))+CJOXQKUMZ7QIdS9@#-}7y2K1{8)cCt0~-X0O!O?Qx#E4Og+;A2SjalQ
zs7r?qn0H044=sDN$SRG$arw~n=+T_DNdSrarmu)V6@|?1-ZB#hRn`uilTGPJ@fqEy
zGt(f0B+^JDP&f=r{#Y_wi#AVDf-y!RIXU^0jXsFpf>=Ji*TeqSY!H~AMbJdCGLhC)
zn7Rx+sXw6uYj;WRYrLd^5IZq@6JI1C^YkgnedZEYy<&4(z%Q$5yv#Boo{AH8n$a
zhb4Y3PWdr269&?V%uI$xMcUrMzl=;w<_nm*qr=c3Rl@i5wWB;e-`t7D&c-mcQl7x!
zZWB`UGcw=Y2=}~wzrfLx=uet<;m3~=8I~ZRuzvMQUQdr+yTV|ATf1Uuomr__nDf=X
zZ3WYJtHp_ri(}SQAPjv+Y+0=fH4krOP@S&=zZ-t1jW1o@}z;xk8
z(Nz1co&El^HK^NrhVHa-_;&88vTU>_J33=%{if;BEY*J#1n59=07jrGQ#IP>@u#3A
z;!q+E1Rj3ZJ+!4bq9F8PXJ@yMgZL;>&gYA0%_Kbi8?S=XGM~dnQZQ!yBSgcZhY96H
zrWnU;k)qy`rX&&xlDyA%(a1Hhi5CWkmg(`Gb%m(HKi-7Z!LKGRP_B8@`7&hdDy5n=
z`OIxqxiVfX@OX1p(mQu>0Ai*v_cTMiw4qRt3~NBvr9oBy0)r>w3p~V0SCm=An6@3n)>@z!|o-$HvDK
z|3D2ZMJkLE5loMKl6R^ez@Zz%S$&mbeoqH5`Bb){Ei21q&VP)hWS2tjShfFtGE+$z
zzCR$P#uktu+#!w)cX!lWN1XU%K-r=s{|j?)Akf@q#3b#{6cZCuJ~gCxuMXRmI$nGtnH+-h
z+GEi!*X=AP<|fG`1>MBdTb?28JYc=fGvAi2I<$B(rs$;eoJCyR6_bc~p!XR@O-+sD
z=eH`-ye})I5ic1eL~TDmtfJ|8`0VJ*Yr=hNCd)G1p2MMz4C3^Mj?7;!w|Ly%JqmuW
zlIEW^Ft%z?*|fpXda>Jr^1noFZEwFgVV%|*XhH@acv8rdGxeEX{M$(vG{Zw+x(ei@
zmfXb22}8-?Fi`vo-YVrTH*C?a8%M=Hv9MqVH7H^J$KsD?>!SFZ;ZsvnHr_gn=7acz
z#W?0eCdVhVMWN12VV^$>WlQ?f;P^{(&pYTops|btm6aj>_Uz+hqpGwB)vWp0Cf5y<
zft8-je~nn?W11plq}N)4A{l8I7$!ks_x$PXW-2XaRFswX_BnF{R#6YIwMhAgd5F9X
zGmwdadS6(a^fjHtXg8=l?Rc0Sm%hk6E9!5cLVloEy4eh(=FwgP`)~I^5~pBEWo+F6
zSf2ncyMurJN91#cJTy_u8Y}@%!bq1RkGC~-bV@SXRd4F{R-*V`bS+6;W5vZ(&+I<9$;-V|eNfLa5n-6%
z2(}&uGRF;p92eS*sE*oR$@pexaqr*meB)VhmIg@h{uzkk$9~qh#cHhw#>O%)b@+(|
z^IQgqzuj~Sk(J;swEM-3TrJAPCq9k^^^`q{IItKBRXYe}e0Tdr=Huf7da3$l4PdpwWDop%^}n;dD#K4s#DYA8SHZ
z&1!riV4W4R7R#C))JH1~axJ)RYnM$$lIR%6fIVA@zV{XVyx}C+a-Dt8Y9M)^KU0+H
zR4IUb2CJ{Hg>CuaXtD50jB(_Tcx=Z$^WYu2u5kubqmwp%drJ6
z?Fo40g!Qd<-l=TQxqHEOuPX0;^z7iX?Ke^a%XT<13TA^5`4Xcw6D@Ur&VT&CUe0d}
z1GjOVF1^L@>O)l@?bD~$wzgf(nxX1OGD8fEV?TdJcZc2KoUe|oP1#=$$7ee|xbY)A
zDZq+cuTpc(fFdj^=!;{k03C69lMQ(|>uhRfRu%+!k&YOi-3|1QKB
z
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
Xviia!U7SGha1wx#SCgwmn*{w2TRX*I
literal 0
HcmV?d00001
diff --git a/sequence_python/static/description/index.html b/sequence_python/static/description/index.html
new file mode 100644
index 00000000000..ce598bd4d49
--- /dev/null
+++ b/sequence_python/static/description/index.html
@@ -0,0 +1,455 @@
+
+
+
+
+
+
+Sequence from Python expression
+
+
+
+
+
Sequence from Python expression
+
+
+

+
This module allows to generate a sequence by a Python formula expression.
+
Besides common Python functions and operators, it provides several functions
+like ‘random’ and ‘uuid’ in the expression, as well as variables such as the
+next number in the sequence. These are also listed next to the input field
+on the sequence form view.
+
If you want to add more variables for use in the expression, you can extend
+this module.
+
Use cases for this module could be:
+
+- You want to generate alphanumeric numbering
+- You want to apply some math to the number to prevent customers from knowing
+their place in the sequence
+- You want to use UUID
+
+
Table of contents
+
+
+
+
To use this module, you need to:
+
+- Go to the form view of an ir.sequence record
+- Go to the Python tab
+- Enable the ‘Use Python’ checkbox
+- Change the default ‘number’ expression to something more fancy.
+
+
Examples:
+
+# To separate the Odoo-generated number with hyphens eg. 0-0-0-0-1
+'-'.join(number_padded)
+
+# To have an UUID as the sequence value
+uuid.uuid4().hex
+
+# To use an 8-digit binary number
+'{0:#010b}'.format(number + 300)[2:]
+
+
And so on.
+
+
+
+
Bugs are tracked on GitHub Issues.
+In case of trouble, please check there if your issue has already been reported.
+If you spotted it first, help us smashing it by providing a detailed and welcomed
+feedback.
+
Do not contact contributors directly about support or help with technical issues.
+
+
+
+
+
+
+
+
This module is maintained by the OCA.
+

+
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/server-tools project on GitHub.
+
You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.
+
+
+
+
+
diff --git a/sequence_python/tests/__init__.py b/sequence_python/tests/__init__.py
new file mode 100644
index 00000000000..b32ac6c6ab7
--- /dev/null
+++ b/sequence_python/tests/__init__.py
@@ -0,0 +1,3 @@
+# Copyright 2020 ACSONE SA/NV ()
+# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
+from . import test_ir_sequence
diff --git a/sequence_python/tests/test_ir_sequence.py b/sequence_python/tests/test_ir_sequence.py
new file mode 100644
index 00000000000..29a083da5d4
--- /dev/null
+++ b/sequence_python/tests/test_ir_sequence.py
@@ -0,0 +1,39 @@
+# Copyright 2020 ACSONE SA/NV ()
+# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
+
+from odoo.tests.common import SavepointCase
+
+
+class TestIrSequence(SavepointCase):
+ @classmethod
+ def setUpClass(cls):
+ super().setUpClass()
+ cls.Sequence = cls.env["ir.sequence"]
+ cls.sequence = cls.Sequence.create(
+ {
+ "name": "Test sequence",
+ "implementation": "standard",
+ "code": "test.python.sequence",
+ "prefix": "A",
+ "padding": 2,
+ "number_next": 1,
+ "number_increment": 1,
+ "company_id": False,
+ "use_python_code": True,
+ "python_code": "'B' + number_padded + 'C'",
+ }
+ )
+
+ def test_standard_sequence(self):
+ self.assertEquals(self.sequence.python_code_preview, "AB01C")
+ next_number = self.sequence._next()
+ self.assertEquals(next_number, "AB01C")
+ next_number = self.sequence._next()
+ self.assertEquals(next_number, "AB02C")
+
+ def test_nogap_sequence(self):
+ self.sequence.write(dict(implementation="no_gap"))
+ next_number = self.sequence._next()
+ self.assertEquals(next_number, "AB01C")
+ next_number = self.sequence._next()
+ self.assertEquals(next_number, "AB02C")
diff --git a/sequence_python/views/ir_sequence.xml b/sequence_python/views/ir_sequence.xml
new file mode 100644
index 00000000000..743a628b8e0
--- /dev/null
+++ b/sequence_python/views/ir_sequence.xml
@@ -0,0 +1,52 @@
+
+
+
+ ir.sequence
+
+
+
+
+
+
+
+
+
+
+
Help with Python expressions
+
The expression you type here will be evaluated as the next number. The following variables can be used:
+
+ number: The next number of the sequence (integer)
+ number_padded: Padded string of the next number of the sequence
+ sequence: Odoo record of the sequence being used
+ uuid: The Python uuid module, eg. to use uuid.uuid4()
+ random: The Python random module, eg. to use random.randint(0, 9)
+ string: The Python string module, eg. to use random.choices(string.ascii_letters + string.digits, k=4)
+
+
Aside from this, you may use several builtin Python functions
+
+
+
+
+
+
From e8fede3d982004944abd5db7fcfe9c62d6b90140 Mon Sep 17 00:00:00 2001
From: Francesco Apruzzese
Date: Thu, 23 Sep 2021 16:55:03 +0200
Subject: [PATCH 02/16] [MIG] sequence_python: Migration to 14.0
---
sequence_python/README.rst | 11 +++++-----
sequence_python/__manifest__.py | 2 +-
sequence_python/i18n/sequence_python.pot | 22 ++++++++++++++++++-
sequence_python/models/ir_sequence.py | 15 +++++++++----
sequence_python/readme/CONTRIBUTORS.rst | 1 +
sequence_python/static/description/index.html | 7 +++---
sequence_python/tests/test_ir_sequence.py | 11 +++++-----
7 files changed, 50 insertions(+), 19 deletions(-)
diff --git a/sequence_python/README.rst b/sequence_python/README.rst
index 51a1cdbd471..ae63ee5e916 100644
--- a/sequence_python/README.rst
+++ b/sequence_python/README.rst
@@ -14,13 +14,13 @@ Sequence from Python expression
:target: http://www.gnu.org/licenses/agpl-3.0-standalone.html
:alt: License: AGPL-3
.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fserver--tools-lightgray.png?logo=github
- :target: https://github.com/OCA/server-tools/tree/13.0/sequence_python
+ :target: https://github.com/OCA/server-tools/tree/14.0/sequence_python
:alt: OCA/server-tools
.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png
- :target: https://translation.odoo-community.org/projects/server-tools-13-0/server-tools-13-0-sequence_python
+ :target: https://translation.odoo-community.org/projects/server-tools-14-0/server-tools-14-0-sequence_python
:alt: Translate me on Weblate
.. |badge5| image:: https://img.shields.io/badge/runbot-Try%20me-875A7B.png
- :target: https://runbot.odoo-community.org/runbot/149/13.0
+ :target: https://runbot.odoo-community.org/runbot/149/14.0
:alt: Try me on Runbot
|badge1| |badge2| |badge3| |badge4| |badge5|
@@ -78,7 +78,7 @@ Bug Tracker
Bugs are tracked on `GitHub Issues `_.
In case of trouble, please check there if your issue has already been reported.
If you spotted it first, help us smashing it by providing a detailed and welcomed
-`feedback `_.
+`feedback `_.
Do not contact contributors directly about support or help with technical issues.
@@ -94,6 +94,7 @@ Contributors
~~~~~~~~~~~~
* Tom Blauwendraat
+* Francesco Apruzzese
Maintainers
~~~~~~~~~~~
@@ -108,6 +109,6 @@ 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/server-tools `_ project on GitHub.
+This module is part of the `OCA/server-tools `_ project on GitHub.
You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.
diff --git a/sequence_python/__manifest__.py b/sequence_python/__manifest__.py
index 9b6b1eeb83e..0365689a90d 100644
--- a/sequence_python/__manifest__.py
+++ b/sequence_python/__manifest__.py
@@ -5,7 +5,7 @@
"summary": """Calculate a sequence number from a Python expression""",
"author": "Sunflower IT,Odoo Community Association (OCA)",
"website": "https://github.com/OCA/server-tools",
- "version": "13.0.1.0.0",
+ "version": "14.0.1.0.0",
"license": "AGPL-3",
"category": "Generic Modules",
"depends": ["base"],
diff --git a/sequence_python/i18n/sequence_python.pot b/sequence_python/i18n/sequence_python.pot
index 87cb2ef655e..38107f936f4 100644
--- a/sequence_python/i18n/sequence_python.pot
+++ b/sequence_python/i18n/sequence_python.pot
@@ -4,7 +4,7 @@
#
msgid ""
msgstr ""
-"Project-Id-Version: Odoo Server 13.0\n"
+"Project-Id-Version: Odoo Server 14.0\n"
"Report-Msgid-Bugs-To: \n"
"Last-Translator: \n"
"Language-Team: \n"
@@ -55,11 +55,26 @@ msgstr ""
msgid "Aside from this, you may use several"
msgstr ""
+#. module: sequence_python
+#: model:ir.model.fields,field_description:sequence_python.field_ir_sequence__display_name
+msgid "Display Name"
+msgstr ""
+
#. module: sequence_python
#: model_terms:ir.ui.view,arch_db:sequence_python.sequence_view
msgid "Help with Python expressions"
msgstr ""
+#. module: sequence_python
+#: model:ir.model.fields,field_description:sequence_python.field_ir_sequence__id
+msgid "ID"
+msgstr ""
+
+#. module: sequence_python
+#: model:ir.model.fields,field_description:sequence_python.field_ir_sequence____last_update
+msgid "Last Modified on"
+msgstr ""
+
#. module: sequence_python
#: model:ir.model.fields,field_description:sequence_python.field_ir_sequence__python_code_preview
msgid "Preview"
@@ -80,6 +95,11 @@ msgstr ""
msgid "Sequence"
msgstr ""
+#. module: sequence_python
+#: model:ir.model.fields,field_description:sequence_python.field_ir_sequence__smart_search
+msgid "Smart Search"
+msgstr ""
+
#. module: sequence_python
#: model_terms:ir.ui.view,arch_db:sequence_python.sequence_view
msgid ""
diff --git a/sequence_python/models/ir_sequence.py b/sequence_python/models/ir_sequence.py
index 7c1535e7b00..4da75f9fc38 100644
--- a/sequence_python/models/ir_sequence.py
+++ b/sequence_python/models/ir_sequence.py
@@ -4,9 +4,10 @@
import random
import string
import uuid
+from inspect import getmembers, isclass, isfunction
from odoo import fields, models
-from odoo.tools import safe_eval
+from odoo.tools.safe_eval import safe_eval, wrap_module
_logger = logging.getLogger(__name__)
@@ -40,13 +41,19 @@ def _get_python_eval_context(self, number_next):
You can inherit this in your custom module.
:return: tuple
"""
+ wrap_random = wrap_module(random, random.__all__)
+ uuid_elements = [e[0] for e in getmembers(uuid, isfunction)] + [
+ e[0] for e in getmembers(uuid, isclass)
+ ]
+ wrap_uuid = wrap_module(uuid, uuid_elements)
+ wrap_string = wrap_module(string, string.__all__)
return {
"number": number_next[0] if isinstance(number_next, tuple) else number_next,
"number_padded": "%%0%sd" % self.padding % number_next,
"sequence": self,
- "random": random,
- "uuid": uuid,
- "string": string,
+ "random": wrap_random,
+ "uuid": wrap_uuid,
+ "string": wrap_string,
}
def _get_python_value(self, number_next):
diff --git a/sequence_python/readme/CONTRIBUTORS.rst b/sequence_python/readme/CONTRIBUTORS.rst
index 68c0876aa4b..fa027040f9c 100644
--- a/sequence_python/readme/CONTRIBUTORS.rst
+++ b/sequence_python/readme/CONTRIBUTORS.rst
@@ -1 +1,2 @@
* Tom Blauwendraat
+* Francesco Apruzzese
diff --git a/sequence_python/static/description/index.html b/sequence_python/static/description/index.html
index ce598bd4d49..64b6758cd76 100644
--- a/sequence_python/static/description/index.html
+++ b/sequence_python/static/description/index.html
@@ -367,7 +367,7 @@ Sequence from Python expression
!! This file is generated by oca-gen-addon-readme !!
!! changes will be overwritten. !!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -->
-

+

This module allows to generate a sequence by a Python formula expression.
Besides common Python functions and operators, it provides several functions
like ‘random’ and ‘uuid’ in the expression, as well as variables such as the
@@ -422,7 +422,7 @@
Bugs are tracked on GitHub Issues.
In case of trouble, please check there if your issue has already been reported.
If you spotted it first, help us smashing it by providing a detailed and welcomed
-feedback.
+feedback.
Do not contact contributors directly about support or help with technical issues.
@@ -446,7 +447,7 @@
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/server-tools project on GitHub.
+
This module is part of the OCA/server-tools project on GitHub.
You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.
diff --git a/sequence_python/tests/test_ir_sequence.py b/sequence_python/tests/test_ir_sequence.py
index 29a083da5d4..6dd5835929a 100644
--- a/sequence_python/tests/test_ir_sequence.py
+++ b/sequence_python/tests/test_ir_sequence.py
@@ -25,15 +25,16 @@ def setUpClass(cls):
)
def test_standard_sequence(self):
- self.assertEquals(self.sequence.python_code_preview, "AB01C")
+ # import pdb;pdb.set_trace()
+ self.assertEqual(self.sequence.python_code_preview, "AB01C")
next_number = self.sequence._next()
- self.assertEquals(next_number, "AB01C")
+ self.assertEqual(next_number, "AB01C")
next_number = self.sequence._next()
- self.assertEquals(next_number, "AB02C")
+ self.assertEqual(next_number, "AB02C")
def test_nogap_sequence(self):
self.sequence.write(dict(implementation="no_gap"))
next_number = self.sequence._next()
- self.assertEquals(next_number, "AB01C")
+ self.assertEqual(next_number, "AB01C")
next_number = self.sequence._next()
- self.assertEquals(next_number, "AB02C")
+ self.assertEqual(next_number, "AB02C")
From f72c3757b0cb1cf3e4290a04007bf02fc86dc0a7 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Dept=2E=20T=C3=A9cnico?=
Date: Mon, 14 Feb 2022 11:27:56 +0000
Subject: [PATCH 03/16] Added translation using Weblate (Catalan)
---
sequence_python/i18n/ca.po | 129 +++++++++++++++++++++++++++++++
sequence_python/i18n/es_AR.po | 141 ++++++++++++++++++++++++++++++++++
2 files changed, 270 insertions(+)
create mode 100644 sequence_python/i18n/ca.po
create mode 100644 sequence_python/i18n/es_AR.po
diff --git a/sequence_python/i18n/ca.po b/sequence_python/i18n/ca.po
new file mode 100644
index 00000000000..904e5b917f3
--- /dev/null
+++ b/sequence_python/i18n/ca.po
@@ -0,0 +1,129 @@
+# Translation of Odoo Server.
+# This file contains the translation of the following modules:
+# * sequence_python
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: Odoo Server 14.0\n"
+"Report-Msgid-Bugs-To: \n"
+"Last-Translator: Automatically generated\n"
+"Language-Team: none\n"
+"Language: ca\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"
+
+#. module: sequence_python
+#: model_terms:ir.ui.view,arch_db:sequence_python.sequence_view
+msgid "number: The next number of the sequence (integer)"
+msgstr ""
+
+#. module: sequence_python
+#: model_terms:ir.ui.view,arch_db:sequence_python.sequence_view
+msgid ""
+"number_padded: Padded string of the next number of the sequence"
+msgstr ""
+
+#. module: sequence_python
+#: model_terms:ir.ui.view,arch_db:sequence_python.sequence_view
+msgid ""
+"random: The Python random module, eg. to use "
+"random.randint(0, 9)"
+msgstr ""
+
+#. module: sequence_python
+#: model_terms:ir.ui.view,arch_db:sequence_python.sequence_view
+msgid "sequence: Odoo record of the sequence being used"
+msgstr ""
+
+#. module: sequence_python
+#: model_terms:ir.ui.view,arch_db:sequence_python.sequence_view
+msgid ""
+"string: The Python string module, eg. to use "
+"random.choices(string.ascii_letters + string.digits, k=4)"
+msgstr ""
+
+#. module: sequence_python
+#: model_terms:ir.ui.view,arch_db:sequence_python.sequence_view
+msgid ""
+"uuid: The Python uuid module, eg. to use "
+"uuid.uuid4()"
+msgstr ""
+
+#. module: sequence_python
+#: model_terms:ir.ui.view,arch_db:sequence_python.sequence_view
+msgid "Aside from this, you may use several"
+msgstr ""
+
+#. module: sequence_python
+#: model:ir.model.fields,field_description:sequence_python.field_ir_sequence__display_name
+msgid "Display Name"
+msgstr ""
+
+#. module: sequence_python
+#: model_terms:ir.ui.view,arch_db:sequence_python.sequence_view
+msgid "Help with Python expressions"
+msgstr ""
+
+#. module: sequence_python
+#: model:ir.model.fields,field_description:sequence_python.field_ir_sequence__id
+msgid "ID"
+msgstr ""
+
+#. module: sequence_python
+#: model:ir.model.fields,field_description:sequence_python.field_ir_sequence____last_update
+msgid "Last Modified on"
+msgstr ""
+
+#. module: sequence_python
+#: model:ir.model.fields,field_description:sequence_python.field_ir_sequence__python_code_preview
+msgid "Preview"
+msgstr ""
+
+#. module: sequence_python
+#: model_terms:ir.ui.view,arch_db:sequence_python.sequence_view
+msgid "Python Code"
+msgstr ""
+
+#. module: sequence_python
+#: model:ir.model.fields,field_description:sequence_python.field_ir_sequence__python_code
+msgid "Python expression"
+msgstr ""
+
+#. module: sequence_python
+#: model:ir.model,name:sequence_python.model_ir_sequence
+msgid "Sequence"
+msgstr ""
+
+#. module: sequence_python
+#: model:ir.model.fields,field_description:sequence_python.field_ir_sequence__smart_search
+msgid "Smart Search"
+msgstr ""
+
+#. module: sequence_python
+#: model_terms:ir.ui.view,arch_db:sequence_python.sequence_view
+msgid ""
+"The expression you type here will be evaluated as the next number. The "
+"following variables can be used:"
+msgstr ""
+
+#. module: sequence_python
+#: model:ir.model.fields,field_description:sequence_python.field_ir_sequence__use_python_code
+msgid "Use Python"
+msgstr ""
+
+#. module: sequence_python
+#: model:ir.model.fields,help:sequence_python.field_ir_sequence__python_code
+msgid "Write Python code that generates the sequence body."
+msgstr ""
+
+#. module: sequence_python
+#: model_terms:ir.ui.view,arch_db:sequence_python.sequence_view
+msgid "builtin Python functions"
+msgstr ""
+
+#. module: sequence_python
+#: model_terms:ir.ui.view,arch_db:sequence_python.sequence_view
+msgid "number"
+msgstr ""
diff --git a/sequence_python/i18n/es_AR.po b/sequence_python/i18n/es_AR.po
new file mode 100644
index 00000000000..22bc069b2bd
--- /dev/null
+++ b/sequence_python/i18n/es_AR.po
@@ -0,0 +1,141 @@
+# Translation of Odoo Server.
+# This file contains the translation of the following modules:
+# * sequence_python
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: Odoo Server 14.0\n"
+"Report-Msgid-Bugs-To: \n"
+"PO-Revision-Date: 2022-09-05 01:07+0000\n"
+"Last-Translator: Ignacio Buioli \n"
+"Language-Team: none\n"
+"Language: es_AR\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: \n"
+"Plural-Forms: nplurals=2; plural=n != 1;\n"
+"X-Generator: Weblate 4.3.2\n"
+
+#. module: sequence_python
+#: model_terms:ir.ui.view,arch_db:sequence_python.sequence_view
+msgid "number: The next number of the sequence (integer)"
+msgstr "number: El siguiente número de la secuencia (entero)"
+
+#. module: sequence_python
+#: model_terms:ir.ui.view,arch_db:sequence_python.sequence_view
+msgid ""
+"number_padded: Padded string of the next number of the sequence"
+msgstr ""
+"number_padded: Relleno del siguiente número de la secuencia"
+
+#. module: sequence_python
+#: model_terms:ir.ui.view,arch_db:sequence_python.sequence_view
+msgid ""
+"random: The Python random module, eg. to use "
+"random.randint(0, 9)"
+msgstr ""
+"random: El módulo aleatorio de Python, ej. para utilizar "
+"random.randint(0, 9)"
+
+#. module: sequence_python
+#: model_terms:ir.ui.view,arch_db:sequence_python.sequence_view
+msgid "sequence: Odoo record of the sequence being used"
+msgstr ""
+"sequence: Registro Odoo de la secuencia que se está utilizando"
+
+#. module: sequence_python
+#: model_terms:ir.ui.view,arch_db:sequence_python.sequence_view
+msgid ""
+"string: The Python string module, eg. to use "
+"random.choices(string.ascii_letters + string.digits, k=4)"
+msgstr ""
+"string: El módulo string de Python, ej. para utilizar "
+"random.choices(string.ascii_letters + string.digits, k=4)"
+
+#. module: sequence_python
+#: model_terms:ir.ui.view,arch_db:sequence_python.sequence_view
+msgid ""
+"uuid: The Python uuid module, eg. to use "
+"uuid.uuid4()"
+msgstr ""
+"uuid: El módulo uuid de Python, ej. para utilizar uuid."
+"uuid4()"
+
+#. module: sequence_python
+#: model_terms:ir.ui.view,arch_db:sequence_python.sequence_view
+msgid "Aside from this, you may use several"
+msgstr "Aparte de esto, puede utilizar varios"
+
+#. module: sequence_python
+#: model:ir.model.fields,field_description:sequence_python.field_ir_sequence__display_name
+msgid "Display Name"
+msgstr "Mostrar Nombre"
+
+#. module: sequence_python
+#: model_terms:ir.ui.view,arch_db:sequence_python.sequence_view
+msgid "Help with Python expressions"
+msgstr "Ayuda con expresiones Python"
+
+#. module: sequence_python
+#: model:ir.model.fields,field_description:sequence_python.field_ir_sequence__id
+msgid "ID"
+msgstr "ID"
+
+#. module: sequence_python
+#: model:ir.model.fields,field_description:sequence_python.field_ir_sequence____last_update
+msgid "Last Modified on"
+msgstr "Última modificación en"
+
+#. module: sequence_python
+#: model:ir.model.fields,field_description:sequence_python.field_ir_sequence__python_code_preview
+msgid "Preview"
+msgstr "Previsualización"
+
+#. module: sequence_python
+#: model_terms:ir.ui.view,arch_db:sequence_python.sequence_view
+msgid "Python Code"
+msgstr "Código de Python"
+
+#. module: sequence_python
+#: model:ir.model.fields,field_description:sequence_python.field_ir_sequence__python_code
+msgid "Python expression"
+msgstr "Expresión de Python"
+
+#. module: sequence_python
+#: model:ir.model,name:sequence_python.model_ir_sequence
+msgid "Sequence"
+msgstr "Secuencia"
+
+#. module: sequence_python
+#: model:ir.model.fields,field_description:sequence_python.field_ir_sequence__smart_search
+msgid "Smart Search"
+msgstr "Búsqueda Inteligente"
+
+#. module: sequence_python
+#: model_terms:ir.ui.view,arch_db:sequence_python.sequence_view
+msgid ""
+"The expression you type here will be evaluated as the next number. The "
+"following variables can be used:"
+msgstr ""
+"La expresión que escriba acá se evaluará como el siguiente número. Se pueden "
+"utilizar las siguientes variables:"
+
+#. module: sequence_python
+#: model:ir.model.fields,field_description:sequence_python.field_ir_sequence__use_python_code
+msgid "Use Python"
+msgstr "Usar Python"
+
+#. module: sequence_python
+#: model:ir.model.fields,help:sequence_python.field_ir_sequence__python_code
+msgid "Write Python code that generates the sequence body."
+msgstr "Escriba el código de Python que genera el cuerpo de la secuencia."
+
+#. module: sequence_python
+#: model_terms:ir.ui.view,arch_db:sequence_python.sequence_view
+msgid "builtin Python functions"
+msgstr "Funciones de Python incorporadas"
+
+#. module: sequence_python
+#: model_terms:ir.ui.view,arch_db:sequence_python.sequence_view
+msgid "number"
+msgstr "number"
From 42f09fa206382fd13e818e9c0b0a815a2351cd52 Mon Sep 17 00:00:00 2001
From: Emeric Panisset
Date: Wed, 30 Nov 2022 15:00:05 +0100
Subject: [PATCH 04/16] [MIG] sequence_python: Migration to 15.0
---
sequence_python/README.rst | 23 ++++----
sequence_python/__manifest__.py | 2 +-
sequence_python/i18n/sequence_python.pot | 22 +-------
sequence_python/static/description/index.html | 53 ++++++++++---------
4 files changed, 42 insertions(+), 58 deletions(-)
diff --git a/sequence_python/README.rst b/sequence_python/README.rst
index ae63ee5e916..810ffff2e0c 100644
--- a/sequence_python/README.rst
+++ b/sequence_python/README.rst
@@ -2,10 +2,13 @@
Sequence from Python expression
===============================
-.. !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
+..
+ !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! This file is generated by oca-gen-addon-readme !!
!! changes will be overwritten. !!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
+ !! source digest: sha256:a8de58fae139e80e530b657b68f0b305d59789a34359e5932317ec82f227b0eb
+ !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png
:target: https://odoo-community.org/page/development-status
@@ -14,16 +17,16 @@ Sequence from Python expression
:target: http://www.gnu.org/licenses/agpl-3.0-standalone.html
:alt: License: AGPL-3
.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fserver--tools-lightgray.png?logo=github
- :target: https://github.com/OCA/server-tools/tree/14.0/sequence_python
+ :target: https://github.com/OCA/server-tools/tree/15.0/sequence_python
:alt: OCA/server-tools
.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png
- :target: https://translation.odoo-community.org/projects/server-tools-14-0/server-tools-14-0-sequence_python
+ :target: https://translation.odoo-community.org/projects/server-tools-15-0/server-tools-15-0-sequence_python
:alt: Translate me on Weblate
-.. |badge5| image:: https://img.shields.io/badge/runbot-Try%20me-875A7B.png
- :target: https://runbot.odoo-community.org/runbot/149/14.0
- :alt: Try me on Runbot
+.. |badge5| image:: https://img.shields.io/badge/runboat-Try%20me-875A7B.png
+ :target: https://runboat.odoo-community.org/builds?repo=OCA/server-tools&target_branch=15.0
+ :alt: Try me on Runboat
-|badge1| |badge2| |badge3| |badge4| |badge5|
+|badge1| |badge2| |badge3| |badge4| |badge5|
This module allows to generate a sequence by a Python formula expression.
@@ -77,8 +80,8 @@ Bug Tracker
Bugs are tracked on `GitHub Issues `_.
In case of trouble, please check there if your issue has already been reported.
-If you spotted it first, help us smashing it by providing a detailed and welcomed
-`feedback `_.
+If you spotted it first, help us to smash it by providing a detailed and welcomed
+`feedback `_.
Do not contact contributors directly about support or help with technical issues.
@@ -109,6 +112,6 @@ 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/server-tools `_ project on GitHub.
+This module is part of the `OCA/server-tools `_ project on GitHub.
You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.
diff --git a/sequence_python/__manifest__.py b/sequence_python/__manifest__.py
index 0365689a90d..58948382c7a 100644
--- a/sequence_python/__manifest__.py
+++ b/sequence_python/__manifest__.py
@@ -5,7 +5,7 @@
"summary": """Calculate a sequence number from a Python expression""",
"author": "Sunflower IT,Odoo Community Association (OCA)",
"website": "https://github.com/OCA/server-tools",
- "version": "14.0.1.0.0",
+ "version": "15.0.1.0.0",
"license": "AGPL-3",
"category": "Generic Modules",
"depends": ["base"],
diff --git a/sequence_python/i18n/sequence_python.pot b/sequence_python/i18n/sequence_python.pot
index 38107f936f4..2f0749c8ed5 100644
--- a/sequence_python/i18n/sequence_python.pot
+++ b/sequence_python/i18n/sequence_python.pot
@@ -4,7 +4,7 @@
#
msgid ""
msgstr ""
-"Project-Id-Version: Odoo Server 14.0\n"
+"Project-Id-Version: Odoo Server 15.0\n"
"Report-Msgid-Bugs-To: \n"
"Last-Translator: \n"
"Language-Team: \n"
@@ -55,26 +55,11 @@ msgstr ""
msgid "Aside from this, you may use several"
msgstr ""
-#. module: sequence_python
-#: model:ir.model.fields,field_description:sequence_python.field_ir_sequence__display_name
-msgid "Display Name"
-msgstr ""
-
#. module: sequence_python
#: model_terms:ir.ui.view,arch_db:sequence_python.sequence_view
msgid "Help with Python expressions"
msgstr ""
-#. module: sequence_python
-#: model:ir.model.fields,field_description:sequence_python.field_ir_sequence__id
-msgid "ID"
-msgstr ""
-
-#. module: sequence_python
-#: model:ir.model.fields,field_description:sequence_python.field_ir_sequence____last_update
-msgid "Last Modified on"
-msgstr ""
-
#. module: sequence_python
#: model:ir.model.fields,field_description:sequence_python.field_ir_sequence__python_code_preview
msgid "Preview"
@@ -95,11 +80,6 @@ msgstr ""
msgid "Sequence"
msgstr ""
-#. module: sequence_python
-#: model:ir.model.fields,field_description:sequence_python.field_ir_sequence__smart_search
-msgid "Smart Search"
-msgstr ""
-
#. module: sequence_python
#: model_terms:ir.ui.view,arch_db:sequence_python.sequence_view
msgid ""
diff --git a/sequence_python/static/description/index.html b/sequence_python/static/description/index.html
index 64b6758cd76..a0a4dda84ab 100644
--- a/sequence_python/static/description/index.html
+++ b/sequence_python/static/description/index.html
@@ -1,20 +1,19 @@
-
-
+
Sequence from Python expression
+
+
+
+
Base - Write Diff
+
+
+

+
This module allows filtering values to update on records according to
+whether they are actually different from the records’ current values.
+
Table of contents
+
+
+
+
Summary
+
This module allows you to update records by filtering out fields whose
+values are going to be left unchanged by BaseModel.write(); for
+example, let’s assume you have:
+
+>>> self
+sale.order.line(1,)
+>>> self.price_unit
+10.00
+
+
If you use self.write({"price_unit": 10.00}) or
+self.price_unit = 10.00, Odoo may end up executing unnecessary
+operations, like triggering the update on the field, recompute computed
+fields that depend on price_unit, and so on, even if the value is
+actually unchanged.
+
By using this module, you can prevent all of that.
+
You can use this module in 3 different ways. All of them require you to
+add this module as a dependency of your module.
+
1 - Context key ``”write_use_diff_values”``
+
By adding write_use_diff_values=True to the context when updating a
+field value, the BaseModel.write() patch will take care of filtering
+out the fields’ values that are the same as the record’s current ones.
+
⚠️ Beware: the context key is propagated down to other write() calls
+
Example:
+
+from odoo import models
+
+
+class ProductTemplate(models.Model):
+ _inherit = "product.template"
+
+ def write(self, vals):
+ # Update only fields that are actually different
+ self = self.with_context(write_use_diff_values=True)
+ return super().write(vals)
+
+
+class ProductProduct(models.Model):
+ _inherit = "product.product"
+
+ def update_code_if_necessary(self, code: str):
+ # Update ``default_code`` only if different from the current value
+ self.with_context(write_use_diff_values=True).default_code = code
+
+
2 - Method ``BaseModel.write_diff()``
+
It is the same as calling write(), but it automatically enables the
+"write_use_diff_values" context flag: self.write_diff(vals) is a
+shortcut for
+self.with_context(write_use_diff_values=True).write(vals)
+
⚠️ Beware: the context key is propagated down to other write() calls
+
3 - Method ``BaseModel._get_write_diff_values(vals)``
+
This method accepts a write-like dict as param, and returns a new
+dict made of the fields who will actually update the record’s
+values. This allows for a more flexible and customizable behavior than
+the context key usage, because:
+
+- you’ll be able to filter out specific fields, instead of filtering
+out all the fields whose values won’t be changed after the update;
+- you’ll be able to execute the filtering on specific models, instead
+of executing it on all the models involved in the stack of
+write() calls from the first usage of the context key down to the
+base method BaseModel.write().
+
+
Example:
+
+from collections import defaultdict
+
+from odoo import api, models
+from odoo.tools.misc import frozendict
+
+
+class ProductProduct(models.Model):
+ _inherit = "product.product"
+
+ def write(self, vals):
+ # OVERRIDE: ``odoo.addons.product.models.product_product.Product.write()``
+ # override will clear the whole registry cache if either 'active' or
+ # 'product_template_attribute_value_ids' are found in the ``vals`` dictionary:
+ # remove them unless it's necessary to update them
+ fnames = {"active", "product_template_attribute_value_ids"}
+ if vals_to_check := {f: vals.pop(f) for f in fnames.intersection(vals)}:
+ groups = defaultdict(lambda: self.browse())
+ for prod in self:
+ groups[frozendict(prod._get_write_diff_values(vals_to_check))] += prod
+ for diff_vals, prods in groups.items():
+ if res_vals := (vals | dict(diff_vals)):
+ super(ProductProduct, prods).write(res_vals)
+ return True
+ return super().write(vals)
+
+
+
+
+
Bugs are tracked on GitHub Issues.
+In case of trouble, please check there if your issue has already been reported.
+If you spotted it first, help us to smash it by providing a detailed and welcomed
+feedback.
+
Do not contact contributors directly about support or help with technical issues.
+
+
+
+
+
+
+
+
This module is maintained by the OCA.
+
+
+
+
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/server-tools project on GitHub.
+
You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.
+
+
+
+
+
diff --git a/base_write_diff/tests/__init__.py b/base_write_diff/tests/__init__.py
new file mode 100644
index 00000000000..aa320247467
--- /dev/null
+++ b/base_write_diff/tests/__init__.py
@@ -0,0 +1 @@
+from . import test_base_write_diff
diff --git a/base_write_diff/tests/test_base_write_diff.py b/base_write_diff/tests/test_base_write_diff.py
new file mode 100644
index 00000000000..a630dbbccab
--- /dev/null
+++ b/base_write_diff/tests/test_base_write_diff.py
@@ -0,0 +1,423 @@
+# Copyright 2025 Camptocamp SA
+# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl).
+
+from logging import getLogger
+
+from odoo_test_helper import FakeModelLoader
+
+from odoo import api, fields, models
+from odoo.tests import TransactionCase
+from odoo.tools.misc import mute_logger
+
+from odoo.addons.base.tests.common import DISABLED_MAIL_CONTEXT
+
+
+class TestRecordDiffCommon(TransactionCase):
+ @classmethod
+ def setUpClass(cls):
+ super().setUpClass()
+ # Setup env
+ cls.env = cls.env["base"].with_context(**DISABLED_MAIL_CONTEXT).env
+
+ # ``_register_hook()`` is usually called at the end of the test process, but
+ # we need to be able to test it here
+ cls.env["base"]._register_hook()
+
+ # Load test model
+ cls.loader = FakeModelLoader(cls.env, cls.__module__)
+ cls.loader.backup_registry()
+
+ class BWDTestModel(models.Model):
+ _name = "bwd.test.model"
+ _description = "Base Write Diff - Test Model"
+ _test_logger = getLogger("bwd.test.model.log")
+
+ # To test a non-relational field
+ name = fields.Char()
+ # To test single-relational fields
+ m2o_id = fields.Many2one("bwd.test.model")
+ # To test multi-relational fields
+ o2m_ids = fields.One2many("bwd.test.model", inverse_name="m2o_id")
+ m2m_ids = fields.Many2many("bwd.test.model", "test_rel", "id_1", "id_2")
+ # To test computed fields
+ # ``perimeter``: computed, stored field that depends on stored fields
+ # ``area``: computed, non-stored field that depends on stored fields
+ # ``volume``: computed, non-stored field that depends on non-stored fields
+ length = fields.Integer() # pylint: disable=W8105 (Pylint complains this?)
+ width = fields.Integer()
+ height = fields.Integer()
+ perimeter = fields.Integer(compute="_compute_perimeter", store=True)
+ area = fields.Integer(compute="_compute_area", store=False)
+ volume = fields.Integer(compute="_compute_volume", store=False)
+
+ @api.depends("length", "width")
+ def _compute_perimeter(self):
+ self._test_logger.warning("Computing perimeter")
+ for rec in self:
+ rec.perimeter = 2 * (rec.length + rec.width)
+
+ @api.depends("length", "width")
+ def _compute_area(self):
+ self._test_logger.warning("Computing area")
+ for rec in self:
+ rec.area = rec.length * rec.width
+
+ @api.depends("area", "height")
+ def _compute_volume(self):
+ self._test_logger.warning("Computing volume")
+ for rec in self:
+ rec.volume = rec.area * rec.height
+
+ cls.loader.update_registry([BWDTestModel])
+
+ @classmethod
+ def tearDownClass(cls):
+ cls.loader.restore_registry()
+ super().tearDownClass()
+
+ def _create_records(self, count=1):
+ records = self.env["bwd.test.model"].create([{} for _ in range(1, count + 1)])
+ for rec in records:
+ rec.name = f"Record {rec.id}"
+ return records
+
+
+class TestRecordDiff(TestRecordDiffCommon):
+ @mute_logger("bwd.test.model.log")
+ def test_00_get_write_diff_values_simple(self):
+ """Test ``_get_write_diff_values()`` on fields that are not multi-relational"""
+ record = self._create_records()
+ # Try to write the same value on non-relational field
+ # => ``_get_write_diff_values()`` returns an empty dict
+ vals = {"name": record.name}
+ self.assertEqual(record._get_write_diff_values(vals), {})
+ # Try to write another value on non-relational field
+ # => ``_get_write_diff_values()`` returns the same dict
+ vals = {"name": record.name + " something else"}
+ self.assertEqual(record._get_write_diff_values(vals), vals)
+ # Try to write the same value on M2O field
+ # => ``_get_write_diff_values()`` returns an empty dict
+ vals = {"m2o_id": record.m2o_id.id}
+ self.assertEqual(record._get_write_diff_values(vals), {})
+ # Try to write another value on M2O field
+ # => ``_get_write_diff_values()`` returns the same dict
+ vals = {"m2o_id": self._create_records().id}
+ self.assertEqual(record._get_write_diff_values(vals), vals)
+
+ @mute_logger("bwd.test.model.log")
+ def test_10_get_write_diff_values_x2many_command_create(self):
+ """Test ``_get_write_diff_values()`` on fields.Command.create()
+
+ ``_get_write_diff_values()`` always returns the original dict, even after the
+ corecords are actually created (because the values will create a new, different
+ corecord if used on ``write()`` again)
+ """
+ record = self._create_records()
+ vals = {
+ "o2m_ids": [fields.Command.create({"name": "O2M Co-record"})],
+ "m2m_ids": [fields.Command.create({"name": "M2M Co-record"})],
+ }
+ self.assertEqual(record._get_write_diff_values(vals), vals)
+ record.write(vals) # Do the real update => the diff is not empty anyway
+ self.assertEqual(record._get_write_diff_values(vals), vals)
+
+ @mute_logger("bwd.test.model.log")
+ def test_11_get_write_diff_values_x2many_command_update(self):
+ """Test ``_get_write_diff_values()`` on fields.Command.update()
+
+ ``_get_write_diff_values()`` returns only the subset of IDs/values that should
+ be updated
+ """
+ record = self._create_records()
+ # Create and assign 2 corecords to each X2M field
+ record.o2m_ids = o2m_corecords = self._create_records(2)
+ record.m2m_ids = m2m_corecords = self._create_records(2)
+ # Set vals to update 1 corecord on each X2M field
+ vals = {
+ "o2m_ids": [
+ fields.Command.update(o2m_corecords[0].id, {"name": "O2M Corec"}),
+ fields.Command.update(
+ o2m_corecords[1].id, {"name": o2m_corecords[1].name}
+ ),
+ ],
+ "m2m_ids": [
+ fields.Command.update(
+ m2m_corecords[0].id, {"name": m2m_corecords[0].name}
+ ),
+ fields.Command.update(m2m_corecords[1].id, {"name": "M2M Corec"}),
+ ],
+ }
+ # The diff should include only the IDs we want to update, and the fields we are
+ # actually different on them
+ self.assertEqual(
+ record._get_write_diff_values(vals),
+ {
+ "o2m_ids": [
+ fields.Command.update(o2m_corecords[0].id, {"name": "O2M Corec"})
+ ],
+ "m2m_ids": [
+ fields.Command.update(m2m_corecords[1].id, {"name": "M2M Corec"})
+ ],
+ },
+ )
+ record.write(vals) # Do the real update => the diff should be empty now
+ self.assertEqual(record._get_write_diff_values(vals), {})
+
+ @mute_logger("bwd.test.model.log")
+ def test_12_get_write_diff_values_x2many_command_delete(self):
+ """Test ``_get_write_diff_values()`` on fields.Command.delete()
+
+ ``_get_write_diff_values()`` returns only the subset of IDs that should be
+ deleted/unlinked
+ """
+ record = self._create_records()
+ # Create and assign 2 corecords to each X2M field
+ record.o2m_ids = o2m_corecords = self._create_records(2)
+ record.m2m_ids = m2m_corecords = self._create_records(2)
+ # Set vals to delete 1 corecord in each X2M field
+ vals = {
+ "o2m_ids": [fields.Command.delete(o2m_corecords[0].id)],
+ "m2m_ids": [fields.Command.delete(m2m_corecords[1].id)],
+ }
+ # The diff should include only the IDs we want to delete
+ self.assertEqual(
+ record._get_write_diff_values(vals),
+ # Odoo assigns command "delete" or "unlink" according to the field type
+ # and its definition (not important for our purposes here)
+ {
+ "o2m_ids": [fields.Command.delete(o2m_corecords[0].id)],
+ "m2m_ids": [fields.Command.unlink(m2m_corecords[1].id)],
+ },
+ )
+ record.write(vals) # Do the real update => the diff should be empty now
+ self.assertEqual(record._get_write_diff_values(vals), {})
+
+ @mute_logger("bwd.test.model.log")
+ def test_13_get_write_diff_values_x2many_command_unlink(self):
+ """Test ``_get_write_diff_values()`` on fields.Command.unlink()
+
+ ``_get_write_diff_values()`` returns only the subset of IDs that should be
+ deleted/unlinked
+ """
+ record = self._create_records()
+ # Create and assign 2 corecords to each X2M field
+ record.o2m_ids = o2m_corecords = self._create_records(2)
+ record.m2m_ids = m2m_corecords = self._create_records(2)
+ # Set vals to unlink 1 corecord in each X2M field
+ vals = {
+ "o2m_ids": [fields.Command.unlink(o2m_corecords[0].id)],
+ "m2m_ids": [fields.Command.unlink(m2m_corecords[1].id)],
+ }
+ # The diff should include only the IDs we want to unlink
+ self.assertEqual(
+ record._get_write_diff_values(vals),
+ # Odoo assigns command "delete" or "unlink" according to the field type
+ # and its definition (not important for our purposes here)
+ {
+ "o2m_ids": [fields.Command.delete(o2m_corecords[0].id)],
+ "m2m_ids": [fields.Command.unlink(m2m_corecords[1].id)],
+ },
+ )
+ record.write(vals) # Do the real update => the diff should be empty now
+ self.assertEqual(record._get_write_diff_values(vals), {})
+
+ @mute_logger("bwd.test.model.log")
+ def test_14_get_write_diff_values_x2many_command_link(self):
+ """Test ``_get_write_diff_values()`` on fields.Command.link()
+
+ ``_get_write_diff_values()`` returns only the subset of IDs that should be
+ linked
+ """
+ record = self._create_records()
+ # Create 2 corecords
+ o2m_corecords = self._create_records(2)
+ m2m_corecords = self._create_records(2)
+ # Assign 1 corecord to each X2M field
+ record.write(
+ {
+ "o2m_ids": [fields.Command.set(o2m_corecords[0].ids)],
+ "m2m_ids": [fields.Command.set(m2m_corecords[1].ids)],
+ }
+ )
+ # Set vals to link all corecords on each X2M field
+ vals = {
+ "o2m_ids": [fields.Command.link(i) for i in o2m_corecords.ids],
+ "m2m_ids": [fields.Command.link(i) for i in m2m_corecords.ids],
+ }
+ # The diff should include only the IDs we want to link that are not already
+ # linked
+ self.assertEqual(
+ record._get_write_diff_values(vals),
+ # Odoo will update the commands to include the {"id": corecord.id} in them
+ {
+ "o2m_ids": [
+ (
+ fields.Command.LINK,
+ o2m_corecords[1].id,
+ {"id": o2m_corecords[1].id},
+ )
+ ],
+ "m2m_ids": [
+ (
+ fields.Command.LINK,
+ m2m_corecords[0].id,
+ {"id": m2m_corecords[0].id},
+ )
+ ],
+ },
+ )
+ record.write(vals) # Do the real update => the diff should be empty now
+ self.assertEqual(record._get_write_diff_values(vals), {})
+
+ @mute_logger("bwd.test.model.log")
+ def test_15_get_write_diff_values_x2many_command_clear(self):
+ """Test ``_get_write_diff_values()`` on fields.Command.clear()
+
+ ``_get_write_diff_values()`` returns only the subset of IDs that should be
+ deleted/unlinked
+ """
+ record = self._create_records()
+ # Create and assign 2 corecords to each X2M field
+ record.o2m_ids = o2m_corecords = self._create_records(2)
+ record.m2m_ids = m2m_corecords = self._create_records(2)
+ # Set vals to clear each X2M field
+ vals = {
+ "o2m_ids": [fields.Command.clear()],
+ "m2m_ids": [fields.Command.clear()],
+ }
+ self.assertEqual(
+ record._get_write_diff_values(vals),
+ # Odoo assigns command "delete" or "unlink" according to the field type
+ # and its definition (not important for our purposes here)
+ {
+ "o2m_ids": [fields.Command.delete(i) for i in o2m_corecords.ids],
+ "m2m_ids": [fields.Command.unlink(i) for i in m2m_corecords.ids],
+ },
+ )
+ record.write(vals) # Do the real update => the diff should be empty now
+ self.assertEqual(record._get_write_diff_values(vals), {})
+
+ @mute_logger("bwd.test.model.log")
+ def test_16_get_write_diff_values_x2many_command_set(self):
+ """Test ``_get_write_diff_values()`` on fields.Command.set()
+
+ ``_get_write_diff_values()`` behavior depends on various cases
+ """
+ record = self._create_records()
+ # Create 3 corecords for each X2M field
+ o2m_corecords = self._create_records(3)
+ m2m_corecords = self._create_records(3)
+
+ # Case 1:
+ # - X2M fields contain no corecords
+ # - we want to assign them some corecords
+ # => ``_get_write_diff_values()`` should return a ``fields.Command.link()``
+ # command for each corecord to add
+ self.assertEqual(
+ record._get_write_diff_values(
+ {
+ "o2m_ids": [fields.Command.set(o2m_corecords.ids)],
+ "m2m_ids": [fields.Command.set(m2m_corecords.ids)],
+ },
+ ),
+ # Odoo will update the commands to "link", and it will add the
+ # {"id": corecord.id} in them
+ {
+ "o2m_ids": [
+ (fields.Command.LINK, i, {"id": i}) for i in o2m_corecords.ids
+ ],
+ "m2m_ids": [
+ (fields.Command.LINK, i, {"id": i}) for i in m2m_corecords.ids
+ ],
+ },
+ )
+
+ # Case 2:
+ # - X2M fields contain some corecords
+ # - we want to replace them with different corecords
+ # => ``_get_write_diff_values()`` should return a
+ # ``fields.Command.[delete|unlink]()`` command for each corecord to remove,
+ # and a ``fields.Command.link()`` command for each corecord to add
+ record.o2m_ids = o2m_corecords[:1]
+ record.m2m_ids = m2m_corecords[:2]
+ self.assertEqual(
+ record._get_write_diff_values(
+ {
+ "o2m_ids": [fields.Command.set(o2m_corecords[1:].ids)],
+ "m2m_ids": [fields.Command.set(m2m_corecords[2:].ids)],
+ },
+ ),
+ # Odoo will update the commands to "unlink", "delete" and "link" (with the
+ # {"id": corecord.id} in the "link" ones)
+ {
+ "o2m_ids": [
+ (fields.Command.DELETE, i, 0) for i in o2m_corecords[:1].ids
+ ]
+ + [(fields.Command.LINK, i, {"id": i}) for i in o2m_corecords[1:].ids],
+ "m2m_ids": [
+ (fields.Command.UNLINK, i, 0) for i in m2m_corecords[:2].ids
+ ]
+ + [(fields.Command.LINK, i, {"id": i}) for i in m2m_corecords[2:].ids],
+ },
+ )
+
+ # Case 3:
+ # - X2M fields contain some corecords
+ # - we want to reassign the same corecords
+ # => ``_get_write_diff_values()`` should return nothing
+ record.o2m_ids = o2m_corecords
+ record.m2m_ids = m2m_corecords
+ self.assertEqual(
+ record._get_write_diff_values(
+ {
+ "o2m_ids": [fields.Command.set(o2m_corecords.ids)],
+ "m2m_ids": [fields.Command.set(m2m_corecords.ids)],
+ },
+ ),
+ {},
+ )
+
+ # Case 4:
+ # - X2M fields contain some corecords
+ # - we want to remove all corecords
+ # => ``_get_write_diff_values()`` should return a
+ # ``fields.Command.[delete|unlink]()`` command for each linked corecord
+ self.assertEqual(
+ record._get_write_diff_values(
+ {
+ "o2m_ids": [fields.Command.set([])],
+ "m2m_ids": [fields.Command.set([])],
+ },
+ ),
+ # Odoo will update the commands to "unlink" and "delete"
+ {
+ "o2m_ids": [(fields.Command.DELETE, i, 0) for i in o2m_corecords.ids],
+ "m2m_ids": [(fields.Command.UNLINK, i, 0) for i in m2m_corecords.ids],
+ },
+ )
+
+ # pylint: disable=W0104
+ def test_20_write_diff_computed_fields(self):
+ """Checks cache behavior for computed fields when diff-writing their deps"""
+ # Prepare the record, its fields values and the cache
+ record = self._create_records()
+ vals = {"length": 5, "width": 3, "height": 2}
+ record.write(vals)
+ fnames = ("perimeter", "area", "volume")
+ for fname in fnames:
+ with mute_logger("bwd.test.model.log"):
+ record[fname] # Dummy read: set fields in cache
+
+ # Use ``write`` w/ the same values: Odoo will need to recompute the computed
+ # fields values as soon as they're read
+ record.write(vals)
+ for fname in fnames:
+ with self.assertLogs("bwd.test.model.log", level="WARNING"):
+ record[fname] # Dummy read: check the compute method is triggered
+
+ # Use ``write_diff`` w/ the same values: Odoo won't need to recompute the
+ # computed fields values
+ record.write_diff(vals)
+ for fname in fnames:
+ with self.assertNoLogs("bwd.test.model.log", level="WARNING"):
+ record[fname] # Dummy read: check the compute method is not triggered
From 4abf3329afc33ba9f4152a8134c08d2ba3ecd4c5 Mon Sep 17 00:00:00 2001
From: oca-ci
Date: Sat, 6 Dec 2025 21:34:05 +0000
Subject: [PATCH 09/16] [UPD] Update sequence_python.pot
---
sequence_python/i18n/sequence_python.pot | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/sequence_python/i18n/sequence_python.pot b/sequence_python/i18n/sequence_python.pot
index 23379e65e50..76cab3e67da 100644
--- a/sequence_python/i18n/sequence_python.pot
+++ b/sequence_python/i18n/sequence_python.pot
@@ -4,7 +4,7 @@
#
msgid ""
msgstr ""
-"Project-Id-Version: Odoo Server 16.0\n"
+"Project-Id-Version: Odoo Server 17.0\n"
"Report-Msgid-Bugs-To: \n"
"Last-Translator: \n"
"Language-Team: \n"
From 47dfcd7e14dd0527972f5e8c47b06d4a369857e7 Mon Sep 17 00:00:00 2001
From: OCA-git-bot
Date: Sat, 6 Dec 2025 21:40:56 +0000
Subject: [PATCH 10/16] [BOT] post-merge updates
---
README.md | 1 +
sequence_python/README.rst | 8 ++++--
sequence_python/static/description/index.html | 28 +++++++++++--------
setup/_metapackage/pyproject.toml | 3 +-
4 files changed, 26 insertions(+), 14 deletions(-)
diff --git a/README.md b/README.md
index 78b5c749dfb..20a580893fc 100644
--- a/README.md
+++ b/README.md
@@ -53,6 +53,7 @@ addon | version | maintainers | summary
[rpc_helper](rpc_helper/) | 17.0.1.0.0 |
| Helpers for disabling RPC calls
[scheduler_error_mailer](scheduler_error_mailer/) | 17.0.1.0.0 | | Scheduler Error Mailer
[sentry](sentry/) | 17.0.1.0.0 |
| Report Odoo errors to Sentry
+[sequence_python](sequence_python/) | 17.0.1.0.0 | | Calculate a sequence number from a Python expression
[server_action_logging](server_action_logging/) | 17.0.1.0.0 | | Module that provides a logging mechanism for server actions
[session_db](session_db/) | 17.0.1.0.1 |
| Store sessions in DB
[slow_statement_logger](slow_statement_logger/) | 17.0.1.0.0 | | Log slow SQL statements
diff --git a/sequence_python/README.rst b/sequence_python/README.rst
index 90270f598f4..038be777be1 100644
--- a/sequence_python/README.rst
+++ b/sequence_python/README.rst
@@ -1,3 +1,7 @@
+.. image:: https://odoo-community.org/readme-banner-image
+ :target: https://odoo-community.org/get-involved?utm_source=readme
+ :alt: Odoo Community Association
+
===============================
Sequence from Python expression
===============================
@@ -7,13 +11,13 @@ Sequence from Python expression
!! This file is generated by oca-gen-addon-readme !!
!! changes will be overwritten. !!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
- !! source digest: sha256:2577b1699eaf32186615d1426d69a461c185cd70423a76700e336c09cf5c7056
+ !! source digest: sha256:ec9931487f48ecf99466381c638107cc1cded6841dad03511386aad24ace10ee
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
.. |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
+.. |badge2| image:: https://img.shields.io/badge/license-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%2Fserver--tools-lightgray.png?logo=github
diff --git a/sequence_python/static/description/index.html b/sequence_python/static/description/index.html
index 6e4d81b6938..f4bd5711ad2 100644
--- a/sequence_python/static/description/index.html
+++ b/sequence_python/static/description/index.html
@@ -3,7 +3,7 @@
-Sequence from Python expression
+README.rst
-
-
Sequence from Python expression
+
+
+
+
+
+
+
Sequence from Python expression
-

+

This module allows to generate a sequence by a Python formula
expression.
Besides common Python functions and operators, it provides several
@@ -399,7 +404,7 @@
Sequence from Python expression
-
+
To use this module, you need to:
- Go to the form view of an ir.sequence record
@@ -421,7 +426,7 @@
And so on.
-
+
Bugs are tracked on GitHub Issues.
In case of trouble, please check there if your issue has already been reported.
If you spotted it first, help us to smash it by providing a detailed and welcomed
@@ -429,22 +434,22 @@
Do not contact contributors directly about support or help with technical issues.
+