Vous êtes sur la page 1sur 123

Odoo 10 Development

Deploy
Odoo config file
Run Odoo test
Make startup script
Create Alias by Nginx
Apaches config
Notice for Image files
Odoo data_dir config
Nginx config
Backup Odoo
Backup database
Backup file store
Download Project addons
Restore Odoo
Import database
Copy Filestore
Copy Project addons
Common Postgres commands

Python
Helpful tips
Check if model has function
getattr
hasattr
CALENDAR and TIME ZONE
+7:00 Get time with time zone with fields.Datetime.context_timestamp
-7:00 Convert timezone to UTC
Get all Calendar Attendance of a date
Get total calendar working hours of a date
Get Calendar working intervals of day
Lambda
Filter
Reduce
Default
Useful methods
Tips & Tricks
Union |=
For loop
Time zone
UTC to User local time
User local time to UTC

Models
Default fields
Fields
Date and Datetime
fields.Date.today() and fields.Datetime.now() returns wrong values
One2many
Many2one
Field Attributes
Attributes
Relation field attributes
ondelete
context and domain
Computed Fields
Search
Related Fields
Parent Child relation
Constraint
Database constraints
Server python constraints
Delegation Inheritance
Create
Write
Unlink
Controller Inherit

Javascript
Call order of functions

Function
Predefined functions
name_get
_name_search
Create
Write
Field_get
fields_view_get
name_search
Decorator
Hiding methods from the RPC interface
@api.one (deprecated)
@api.multi
@api.model
@api.returns
@api.onchange
Execute SQL
Actions
ir.actions.act_window
Menu action
Return to client
Context in action

ORM
Create new record
Updating values of recordset records
Write values
Searching
Combining Recordsets
Filter
Mapped

Views
Field
State
Domain
Domain ID in One2many
Options
no_create
no_quick_create
no_create_edit
reload_on_button and always_reload
Context
default values
Tree_view_ref & Form_view_ref
Hide tree view column based on context
Attributes
Invisible
Required
Widgets
Others
separator
Form
Tree
Delete
Create
Editable
Dynamic readonly
One2many select only
Colors
Search
Inherit
Xpath
Attributes
Groups
Invisible
Widgets
X2many widgets
many2many
many2many_tags
many2many_checkboxes
many2many_kanban
x2many_counter
many2many_binary
Action with selected views
Function

Security
User groups
Field access
Views inherit for Groups

Other Features
Environment
Context
Import Data
data/default_data.xml
To reference in module:
Field Domain:
View.xml
Button action from import data
Domain
Today condition

Server

Flow
Sale.order -> MO
Mindmap
Simple
Code inside

Feature development
Check if a module is installed
Wizard
Button open Form
Smart buttons
Tree list button trigger Wizard
casting.py
Casting_scrap.xml
Casting_scrap.py
Config setting
Config.py
Config.xml
Resource.Calendar
Get_working_hours
Get_working_intervals_of_day
Sequence
CRON
Models.py
Cron.xml
Mail template
__manifest__.py
Template.xml
Config outgoing mail
Models.py
Views.xml
Result
PYTHON ESC-POS (python 3)

Deploy
1. Odoo config file
File: /etc/odoo/​odoo10.as.conf

# nano /etc/odoo/odoo10.as.conf
Then past below content

[options]
; This is the password that allows database operations:
; admin_passwd = admin
admin_passwd = some_password
db_host = False
db_port = False
;db_name = v10as01
dbfilter = v10as01
db_user = odoo
db_password = False
list_db = True
addons_path = /opt/odoo/odoo10/addons,/opt/odoo/odoo10/feosco,/opt/odoo/odoo10/myaddons
data_dir = /opt/odoo/odoo10/data
xmlrpc_port = 9101
longpolling_port = 9102

; Log Settings
logfile = /var/log/odoo/odoo10.as.log
Ctrl + O to write file
Ctr + X to save and exit file

Reference feosco.conf

[options]
admin_passwd = xxx
db_port = 5432
db_user = odoo10
db_password = xxx
#dbfilter = feos$
dbfilter = feosco_main

addons_path = /opt/odoo/10.0/odoo/addons,/opt/odoo/10.0/addons,/opt/custom/addons/10.0
logfile = /var/log/odoo/odoo10-feos.log
geoip_database = /opt/custom/GeoLiteCity.dat
data_dir = /opt/custom/data/10.0
xmlrpc = True
xmlrpc_interface =
xmlrpc_port = 8169
proxy_mode = True
xmlrpcs = True
xmlrpcs_interface =
xmlrpcs_port = 8171
#secure_cert_file = server.cert
#secure_pkey_file = server.pkey
netrpc = False
netrpc_interface = 127.0.0.1
netrpc_port = 8170
static_http_enable = False
static_http_document_root = None
static_http_url_prefix = None
test_file = False
test_report_directory = False
test_disable = False
test_commit = False
logrotate = True
syslog = False
log_handler = [[:INFO]]
#log_level = debug
log_level = warn
auto-reload = True
limit-request = 16392
limit-memory-soft = 1342177280
limit-memory-hard = 1610612736
max-cron-threads = 4

2. Run Odoo test


Switch to ​odoo​ user from root:
# su - odoo -s /bin/bash
$ cd /opt/odoo/odoo10
$ ./odoo-bin -c /etc/odoo/odoo10.as.conf

Then open browser to enter the Odoo site: ​http://103.7.40.183:9101


Database creation info:
Master password: some_password
Admin account: admin
Admin pass: feosco

3. Make startup script

Create new script at ​/etc/init.d/odoo10-as​ with below content:


#!/bin/bash
### BEGIN INIT INFO
# Provides: odoo
# Required-Start: $remote_fs $syslog
# Required-Stop: $remote_fs $syslog
# Default-Start: 2 3 4 5
# Default-Stop: 016
# Short-Description: Start odoo daemon at boot time
# Description: Enable service provided by daemon.
# X-Interactive: true
### END INIT INFO
## more info: http://wiki.debian.org/LSBInitScripts

. /lib/lsb/init-functions

PATH=/sbin:/bin:/usr/sbin:/usr/bin:/usr/local/bin
DAEMON=/opt/odoo/odoo10/odoo-bin
NAME=​odoo10-as
DESC=​odoo10-as
CONFIG=​/etc/odoo/odoo10.as.conf
LOGFILE=​/var/log/odoo/odoo10.as.log
PIDFILE=/var/run/${NAME}.pid
USER=odoo
export LOGNAME=$USER

test -x $DAEMON || exit 0


set -e

function _start() {
start-stop-daemon --start --quiet --pidfile $PIDFILE --chuid $USER:$USER --background --make-pidfile
--exec $DAEMON -- --config $CONFIG --logfile $LOGFILE
}

function _stop() {
start-stop-daemon --stop --quiet --pidfile $PIDFILE --oknodo --retry 3
rm -f $PIDFILE
}

function _status() {
start-stop-daemon --status --quiet --pidfile $PIDFILE
return $?
}
case "$1" in
start)
echo -n "Starting $DESC: "
_start
echo "ok"
;;
stop)
echo -n "Stopping $DESC: "
_stop
echo "ok"
;;
restart|force-reload)
echo -n "Restarting $DESC: "
_stop
sleep 1
_start
echo "ok"
;;
status)
echo -n "Status of $DESC: "
_status && echo "running" || echo "stopped"
;;
*)
N=/etc/init.d/$NAME
echo "Usage: $N {start|stop|restart|force-reload|status}" >&2
exit 1
;;
esac

exit 0

Make the script run at startup:


Ref: ​http://askubuntu.com/questions/19320/how-to-enable-or-disable-services
For ubuntu 14.04:
# update-rc.d ​odoo10-as​ defaults
Then start script:
# service odoo10-as start

For Ubuntu 16.04:


# systemctl enable ​odoo10-as
Then start script:
# systemctl start odoo10-as

Checking all startup service


# service --status-all
[ + ] acpid
[ - ] alsa-utils
[ - ] anacron
[ + ] apache-htcacheclean
[ + ] apache2
[ + ] apparmor
[ + ] apport
[ - ] asterisk
[ + ] avahi-daemon
[ - ] bluetooth
[ - ] bootmisc.sh
[ - ] brltty
[ - ] checkfs.sh
[ - ] checkroot-bootclean.sh
[ - ] checkroot.sh
[ + ] console-setup
[ + ] cron
[ + ] cups
[ + ] cups-browsed
[ + ] dbus
[ - ] dns-clean
[ + ] grub-common
[-] hostname.sh
[-] hwclock.sh
[+] irqbalance
[?] jira
[-] kerneloops
[-] keyboard-setup.dpkg-bak
[-] killprocs
[+] kmod
[+] lightdm
[-] lvm2
[+] lvm2-lvmetad
[+] lvm2-lvmpolld
[-] mountall-bootclean.sh
[-] mountall.sh
[-] mountdevsubfs.sh
[-] mountkernfs.sh
[-] mountnfs-bootclean.sh
[-] mountnfs.sh
[+] network-manager
[+] networking
[?] odoo10
[?] odoo102
[?] odoo9
[+] ondemand
[+] php7.0-fpm
[-] plymouth
[-] plymouth-log
[+] postfix
[+] postgresql
[-] pppd-dns
[+] procps
[+] rc.local
[+] resolvconf
[-] rsync
[+] rsyslog
[-] saned
[-] sendsigs
[+] speech-dispatcher
[+] ssh
[+] sysstat
[-] thermald
[+] tomcat7
[+] udev
[+] ufw
[-] umountfs
[-] umountnfs.sh
[-] umountroot
[+] unattended-upgrades
[+] urandom
[-] uuidd
[+] whoopsie
[-] x11-common

4. Create Alias by Nginx


Create a new file at /etc/nginx/site-available/as.feosco.com.9101
Notice: as.feosco.com.9101 file name is a demo, we can put any like as.feosco

server {
listen 80;
server_name as.feosco.com;
location / {
client_max_body_size 10M;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Forwarded-Server $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_pass http://127.0.0.1:9101;
}
}

Test Nginx config:


root@vps40183 /etc/nginx/sites-available # nginx -t
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful

Then link the new config to Enable folder


# ln -s /etc/nginx/sites-available/as.feosco.com.9101 /etc/nginx/sites-enabled/

Then reload Nginx to make it updated


# nginx -s reload

5. Apaches config
Example 1:
<VirtualHost *:80>
LogLevel warn
<Proxy *>
Order deny,allow
Allow from all
</Proxy>

ProxyPass / http://localhost:8169/
ProxyPassReverse / http://localhost:8169/
RewriteEngine on
RewriteCond %{HTTP_HOST} !^(portal\.upscience-labs\.com)?$ [NC]
RewriteCond %{HTTP_HOST} !^(localhost)?$
RewriteRule ^/?(.*)?$ http://portal.upscience-labs.com/$1 [R,L,NE]
ServerName portal.upscience-labs.com
ServerAlias portal.upscience-labs.com
</VirtualHost>

<VirtualHost *:80>
LogLevel warn
<Proxy *>
Order deny,allow
Allow from all
</Proxy>

ProxyPass / http://localhost:8269/
ProxyPassReverse / http://localhost:8269/
RewriteEngine on
RewriteCond %{HTTP_HOST} !^(internal\.upscience-labs\.com)?$ [NC]
RewriteCond %{HTTP_HOST} !^(localhost)?$
RewriteRule ^/?(.*)?$ http://internal.upscience-labs.com/$1 [R,L,NE]
ServerName internal.upscience-labs.com
ServerAlias internal.upscience-labs.com
</VirtualHost>

Example 2:

<VirtualHost *:80>
LogLevel warn
<Proxy *>
Order deny,allow
Allow from all
</Proxy>

ProxyPass / http://localhost:8169/
ProxyPassReverse / http://localhost:8169/
RewriteEngine on
RewriteCond %{HTTP_HOST} !^(www\.feosco\.com)?$ [NC]
RewriteRule ^/?(.*)?$ http://www.feosco.com/$1 [R,L,NE]
ServerName feosco.com
ServerAlias www.feosco.com
</VirtualHost>

<VirtualHost *:80>
LogLevel warn
<Proxy *>
Order deny,allow
Allow from all
</Proxy>

ProxyPass / http://localhost:8069/
ProxyPassReverse / http://localhost:8069/
RewriteEngine on
RewriteCond %{HTTP_HOST} !^(demo289\.feosco\.com)?$ [NC]
RewriteRule ^/?(.*)?$ http://demo289.feosco.com/$1 [R,L,NE]
ServerName feosco.com
ServerAlias demo289.feosco.com
</VirtualHost>

<VirtualHost *:80>
LogLevel warn
<Proxy *>
Order deny,allow
Allow from all
</Proxy>

ProxyPass / http://localhost:8069/
ProxyPassReverse / http://localhost:8069/
RewriteEngine on
RewriteCond %{HTTP_HOST} !^(demo2\.feosco\.com)?$ [NC]
RewriteRule ^/?(.*)?$ http://demo2.feosco.com/$1 [R,L,NE]
ServerName feosco.com
ServerAlias demo2.feosco.com
</VirtualHost>
<VirtualHost *:80>
LogLevel warn
<Proxy *>
Order deny,allow
Allow from all
</Proxy>

ProxyPass / http://localhost:8069/
ProxyPassReverse / http://localhost:8069/
RewriteEngine on
RewriteCond %{HTTP_HOST} !^(demo3\.feosco\.com)?$ [NC]
RewriteRule ^/?(.*)?$ http://demo3.feosco.com/$1 [R,L,NE]
ServerName feosco.com
ServerAlias demo3.feosco.com
</VirtualHost>

<VirtualHost *:80>
LogLevel warn
<Proxy *>
Order deny,allow
Allow from all
</Proxy>

ProxyPass / http://localhost:8069/
ProxyPassReverse / http://localhost:8069/
RewriteEngine on
RewriteCond %{HTTP_HOST} !^(demo645\.feosco\.com)?$ [NC]
RewriteRule ^/?(.*)?$ http://demo645.feosco.com/$1 [R,L,NE]
ServerName feosco.com
ServerAlias demo645.feosco.com
</VirtualHost>

<VirtualHost *:80>
LogLevel warn
<Proxy *>
Order deny,allow
Allow from all
</Proxy>

ProxyPass / http://localhost:8069/
ProxyPassReverse / http://localhost:8069/
RewriteEngine on
RewriteCond %{HTTP_HOST} !^(demo556\.feosco\.com)?$ [NC]
RewriteRule ^/?(.*)?$ http://demo556.feosco.com/$1 [R,L,NE]
ServerName feosco.com
ServerAlias demo556.feosco.com
</VirtualHost>

<VirtualHost *:80>
LogLevel warn
<Proxy *>
Order deny,allow
Allow from all
</Proxy>

ProxyPass / http://localhost:8069/
ProxyPassReverse / http://localhost:8069/
RewriteEngine on
RewriteCond %{HTTP_HOST} !^(demo467\.feosco\.com)?$ [NC]
RewriteRule ^/?(.*)?$ http://demo467.feosco.com/$1 [R,L,NE]
ServerName feosco.com
ServerAlias demo467.feosco.com
</VirtualHost>

<VirtualHost *:80>
LogLevel warn
<Proxy *>
Order deny,allow
Allow from all
</Proxy>

ProxyPass / http://localhost:8069/
ProxyPassReverse / http://localhost:8069/
RewriteEngine on
RewriteCond %{HTTP_HOST} !^(demo734\.feosco\.com)?$ [NC]
RewriteRule ^/?(.*)?$ http://demo734.feosco.com/$1 [R,L,NE]
ServerName feosco.com
ServerAlias demo734.feosco.com
</VirtualHost>

<VirtualHost *:80>
LogLevel warn
<Proxy *>
Order deny,allow
Allow from all
</Proxy>
ProxyPass / http://localhost:8069/
ProxyPassReverse / http://localhost:8069/
RewriteEngine on
RewriteCond %{HTTP_HOST} !^(demo912\.feosco\.com)?$ [NC]
RewriteRule ^/?(.*)?$ http://demo912.feosco.com/$1 [R,L,NE]
ServerName feosco.com
ServerAlias demo912.feosco.com
</VirtualHost>

<VirtualHost *:80>
LogLevel warn
<Proxy *>
Order deny,allow
Allow from all
</Proxy>

ProxyPass / http://localhost:8069/
ProxyPassReverse / http://localhost:8069/
RewriteEngine on
RewriteCond %{HTTP_HOST} !^(demo823\.feosco\.com)?$ [NC]
RewriteRule ^/?(.*)?$ http://demo823.feosco.com/$1 [R,L,NE]
ServerName feosco.com
ServerAlias demo823.feosco.com
</VirtualHost>

<VirtualHost *:80>
LogLevel warn
<Proxy *>
Order deny,allow
Allow from all
</Proxy>

ProxyPass / http://localhost:8069/
ProxyPassReverse / http://localhost:8069/
RewriteEngine on
RewriteCond %{HTTP_HOST} !^(demo378\.feosco\.com)?$ [NC]
RewriteRule ^/?(.*)?$ http://demo378.feosco.com/$1 [R,L,NE]
ServerName feosco.com
ServerAlias demo378.feosco.com
</VirtualHost>
6. Notice for Image files

We need to configure Nginx so that images can be loaded by web.

1. Odoo data_dir config


In the configuration file, we need to set Odoo data dir to a visible folder, not hidden (on Linux, hidden
folder starting name with . ,ex: “.data“)
Config file: /etc/odoo/odoo10.as.conf
data_dir = ​/opt/odoo/odoo10/data

2. Nginx config

server {
listen 80 default_server;
listen [::]:80 default_server ipv6only=on;

root /usr/share/nginx/html;
index index.html index.htm;

# Make site accessible from http://localhost/


server_name localhost;

location / {
# First attempt to serve request as file, then
# as directory, then fall back to displaying a 404.
try_files $uri $uri/ =404;
# Uncomment to enable naxsi on this location
# include /etc/nginx/naxsi.rules
}

location /odoo-files/ {
alias /opt/odoo/odoo9/data/filestore/;
autoindex off;
allow all;
}

location ​/odoo10-files/​ {
alias /opt/odoo/odoo10/data/filestore/;
autoindex off;
allow all;
}

6. Backup Odoo

Backup database
Đăng nhập vào user postgres từ tài khoản root
root@ip-172-31-44-166:~# su - postgres
postgres@ip-172-31-44-166:~$

Liệt kê tất cả các Postgres database bằng lệnh "psql -l"


postgres@ip-172-31-44-166:~$ psql -l

Backup dữ database bằng lệnh "pg_dump"

Đầu tiên đi đến thư mục cần lưu dữ liệu , lưu ý thư mục này phải cho phép user postgres tạo file
postgres@ip-172-31-44-166:~$ cd /opt/odoo/odoo10/db_backup/

Backup database
postgres@ip-172-31-44-166:/opt/odoo/odoo10/db_backup$ pg_dump v10tn01 > v10tn01_170805.bak
Dùng File Zilla download về

Backup file store

Các file store được lưu trong thư mục data_dir qui định trong Odoo config file.
vd:
$ cat /opt/odoo/odoo10.conf
....
data_dir=/opt/odoo/data
....

Dùng FileZilla download dữ liệu tương ứng với database .


Các hình upload được lưu trong filestore .

Download Project addons


Dùng Filezilla download tất cả các addons của dự án.
Ví dụ: dự án TN gồm các addons : web_widget_color , jewelry.

7. Restore Odoo
Import database

we can create a new database called "restored_database" and then redirect a dump called "database.bak"
by issuing these commands:
Command line:
postgres@dark:~$ createdb -T template0 v10tn02
postgres@dark:~$ psql v10tn02 < v10tn_170805.bak

Copy Filestore
Copy or Move filestore to expected filestore as in Odoo configuration file
Command line:
root@dark:/opt/odoo10/backup_data# mv v10tn01 /opt/odoo10/data/filestore/v10tn02
Copy Project addons
Tạo thư mục dự án (ví dụ: tn ) và chép các addons của dự án vào thư mục đó.
Cập nhật đường dẫn addons_path ​ trong odoo config file bao gồm thư mục đó.
Vd: /opt/odoo10/projects/tn

File: ​odoo10-tn.conf
[DEFAULT]
root = /opt/odoo10

[options]
; This is the password that allows database operations:
admin_passwd = admin
db_host = False
db_port = False
db_user = odoo
db_password = shipnpay
dbfilter = v10tn01
list_db = False
addons_path = %(root)s/odoo/addons,%(root)s/oe,%(root)s/projects/feosco,​%(root)s/projects/tn
data_dir = %(root)s/data
longpolling_port = 8070
xmlrpc_port = 8069

; Log Settings
;logfile = /var/log/odoo/odoo10.log
;log_level = error

Common Postgres commands

Restart postgresql servvice

# systemctl status postgresql


Using Postgres command
root@~# su - postgres
postgres@Ubuntu:~$ psql // start Postgresql commands

Update table column

root@dark:/home/akn# su - postgres
postgres@dark:~$ psql
List all databases
postgres=# \l

List of databases
Name | Owner | Encoding | Collate | Ctype | Access privileges
-----------+----------+----------+-------------+-------------+-----------------------
dev | odoo | UTF8 | en_US.UTF-8 | en_US.UTF-8 |
garment | odoo | UTF8 | en_US.UTF-8 | en_US.UTF-8 |
greatio10 | odoo | UTF8 | en_US.UTF-8 | en_US.UTF-8 |
postgres | postgres | UTF8 | en_US.UTF-8 | en_US.UTF-8 |
template0 | postgres | UTF8 | en_US.UTF-8 | en_US.UTF-8 | =c/postgres +
| | | | | postgres=CTc/postgres
template1 | postgres | UTF8 | en_US.UTF-8 | en_US.UTF-8 | =c/postgres +
| | | | | postgres=CTc/postgres
tn01 | odoo | UTF8 | en_US.UTF-8 | en_US.UTF-8 |
v10as00 | odoo | UTF8 | en_US.UTF-8 | en_US.UTF-8 |
v10as01 | odoo | UTF8 | en_US.UTF-8 | en_US.UTF-8 |
v10as02 | odoo | UTF8 | en_US.UTF-8 | en_US.UTF-8 |
v10asia01 | odoo | UTF8 | en_US.UTF-8 | en_US.UTF-8 |
v10mech01 | odoo | UTF8 | en_US.UTF-8 | en_US.UTF-8 |
v10test00 | odoo | UTF8 | en_US.UTF-8 | en_US.UTF-8 |
v10test01 | odoo | UTF8 | en_US.UTF-8 | en_US.UTF-8 |
v10tn01 | odoo | UTF8 | en_US.UTF-8 | en_US.UTF-8 |
v10tn02 | postgres | UTF8 | en_US.UTF-8 | en_US.UTF-8 |

Connect to desired database ex: v10tn01


postgres=# \c v10tn01
You are now connected to database "v10tn01" as user "postgres".
v10tn01=# UPDATE mrp_workorder SET workcenter_id=27 WHERE workcenter_id=9;
UPDATE 121

Alter Database owner


Ex: alter ​crio​ database owner to ​akn

root@~# su - postgres
postgres@Ubuntu:~$ psql // start Postgresql commands
postgres=# ALTER DATABASE ​crio​ OWNER TO ​akn​;
Python
Helpful tips
Check if model has function

getattr
Ex:

barcode_scanning = ​getattr​(http.request.env['hr.attendance'],barcode_scanning,None)
if ​callable​(barcode_scanning):
res = barcode_scanning(barcode)
else:
id = self.search_barcode(res_model, field, barcode)

hasattr
Ex:
if ​hasattr​(http.request.env[res_model],'barcode_scanning'):
res = http.request.env[res_model].barcode_scanning(barcode)
else:
id = self.search_barcode(res_model, field, barcode)

CALENDAR and TIME ZONE

+7:00 Get time with time zone with fields.Datetime.context_timestamp

Ex: Chuyển từ UTC sang TimeZone. Ví dụ: 7:30 ==> 14:30


datetime_fmt = "%Y-%m-%d %H:%M:%S"
check_time = fields.Datetime.now()
check_time_tz = ​fields.Datetime.context_timestamp​(
self, timestamp=datetime.strptime(check_time, datetime_fmt)).strftime(datetime_fmt)

Example in Module:
# -*- coding: utf-8 -*-

from odoo import models, fields, api


import datetime

class MyModel(models.TransientModel):
_name = 'my.model'

def ​get_sale_order​(self,soid):

confirmation_date = self.get_context_time_zone(so.confirmation_date)
...

def ​get_context_time_zone​(self,time_str):
datetime_fmt = "%Y-%m-%d %H:%M:%S"
return fields.Datetime.context_timestamp(self,
timestamp=datetime.datetime.strptime(time_str,datetime_fmt)).
strftime(datetime_fmt)

-7:00 Convert timezone to UTC

class ResourceCalendar(models.Model):
_inherit = "resource.calendar"

@api.model
def convert_local_timezone_to_utc(self,user_tz, local_time):
res = False
if user_tz and local_time:
from_zone = tz.gettz(user_tz)
# from_zone = tz.gettz('Asia/Ho_Chi_Minh')
to_zone = tz.gettz('UTC')
utc = datetime.strptime(local_time, '%Y-%m-%d %H:%M:%S')
# Tell the datetime object that it's in UTC time zone since
# datetime objects are 'naive' by default
utc = utc.replace(tzinfo=from_zone)

# Convert time zone


central = utc.astimezone(to_zone)
res = central.strftime('%Y-%m-%d %H:%M:%S')
# print '\n_convert_local_timezone_to_utc return',res
return res
Usage:

check_in = '2017-10-02 7:30:00'


check_in_utc = self.env['resource.calendar'].convert_local_timezone_to_utc(self._context.get('tz'),
check_in)
# return check_in_utc = '2017-10-02 0:30:00'

Get all Calendar Attendance of a date

datetime_fmt = "%Y-%m-%d %H:%M:%S"


check_in = fields.Datetime.context_timestamp(self, timestamp=datetime.strptime(r.check_in,
datetime_fmt))
# check_in is a datetime object
cal_atds = r.employee_id.calendar_id.​get_attendances_for_weekday​(check_in)

Get total calendar working hours of a date

datetime_fmt = "%Y-%m-%d %H:%M:%S"


date_fmt = '%Y-%m-%d'
di = datetime.strptime(self.check_in, date_fmt)
# Get start and end time of the date
di_time0 = datetime.combine(di, datetime.min.time())
di_time1 = datetime.combine(di, datetime.max.time())
full_hours = calendar.​get_working_hours_of_date​(start_dt=di_time0,end_dt=di_time1) ​# 8.0

Get Calendar working intervals of day


date_fmt = "%Y-%m-%d"
leave_date_from = datetime.strptime(date_start,date_fmt)
calendar_intervals = calendar_id.​get_working_intervals_of_day​(leave_date_from)
print '\n --> Calendar Intervals',calendar_intervals

Example: Get min and max working time for a date

@api.multi
def get_union_working_interval_of_day(self,date):
self.ensure_one()
datetime_fmt = '%Y-%m-%d %H:%M:%S'
# date_fmt = '%Y-%m-%d'
leave_date_from = datetime.strptime(date,datetime_fmt)
calendar_intervals = self.get_working_intervals_of_day(leave_date_from)
print '\n --> Calendar Intervals',calendar_intervals
date_from = False
date_to = False
for intv in calendar_intervals:
intv_from = intv[0].strftime(datetime_fmt)
intv_to = intv[1].strftime(datetime_fmt)
date_from = min(date_from, intv_from) if date_from else intv_from
date_to = max(date_to,intv_to) if date_to else intv_to
return (date_from, date_to)

Lambda
Filter

Ex:

metal_attr = self.attribute_value_ids
.​filtered​( ​lambda​ a: a.attribute_id == self.env.ref('jewelry.product_attr_metal_type'))
self.metal_type_id = metal_attr if metal_attr else False

Reduce

Default

color_id = fields.Many2one(
comodel_name='product.option.color',
string='Color',
default=lambda​ self: self.env.ref('jewelry.opt_color_white'))
Useful methods

Methods Description

self.user_has_groups( Check user has a role


‘library.group_library_manager')

self.check_access_rights("read")

self.check_access_rule("read")

Tips & Tricks

Union |=
We use |= to compute the union of the current contacts of the partner and the new contacts passed to the
method.

Example:
@api.model
def add_contacts(self, partner, contacts):
partner.ensure_one()
if contacts:
partner.date = fields.Date.context_today()
partner.child_ids |= contacts
For loop

produc ing_qty = ​sum​(l.qty_produced ​for​ l ​in​ r.tree_line_ids ​if​ l.state not in [‘done',’cancel’])

producing = sum(r.tree_line_ids.filtered(lambda l: l.state in


[‘done',’cancel’]).mapped('qty_produced'))

Time zone
UTC to User local time

from datetime import datetime


from dateutil import tz

def _get_context_time_zone(self,time_str):
if not time_str:
return False
datetime_fmt = "%Y-%m-%d %H:%M:%S"
return fields.Datetime.context_timestamp(self, timestamp=datetime.strptime(time_str,
datetime_fmt)).strftime(datetime_fmt) if time_str else False

User local time to UTC


from datetime import datetime
from dateutil import tz

def _convert_local_timezone_to_utc(self,user_tz, local_time):


# print '\n ---> _convert_local_timezone_to_utc input',user_tz,local_time
res = False
if user_tz and local_time:
from_zone = tz.gettz(user_tz)
# from_zone = tz.gettz('Asia/Ho_Chi_Minh')
to_zone = tz.gettz('UTC')
utc = datetime.strptime(local_time, '%Y-%m-%d %H:%M:%S')
# Tell the datetime object that it's in UTC time zone since
# datetime objects are 'naive' by default
utc = utc.replace(tzinfo=from_zone)
# Convert time zone
central = utc.astimezone(to_zone)
res = central.strftime('%Y-%m-%d %H:%M:%S')
# print '\n_convert_local_timezone_to_utc return',res
return res

Models
Default fields

class ​LibraryBook​(models.Model):
_name = 'library.book'
_description​ = 'Library Book'
_order​ = 'date_release desc, name'
_rec_name​ = 'short_name'
name = fields.Char(‘Title',required=True)
# To use the short_name field as the record representation, add the following:
short_name = fields.Char('Short Title')

def ​name_get​(self):
result = []
for r in self:
result.append( (r.id, u"%s (%s)" % (r.name, r.date_release)) )
return result

Record representation is available in a magic ​display_name​ computed field added automatically to all
models since version 8.0. Its values are generated using the Model
method ​name_get()​ , which was already in existence in previous Odoo versions.
Its default implementation uses the ​_rec_name​ attribute. For more sophisticated representations, we can
override its logic. This method must return a list of tuples with two elements: the ID of the record and the
Unicode string representation for the record.
Ex: (1,’Hello world’)

Do notice that we used a Unicode string while building the record representation, u"%s (%s)" . This is
important to avoid errors, in case we find non-ASCII characters.

A few fields are added by default in Odoo models:


● create_date​ is the record creation timestamp
● create_uid​ is the user that created the record
● write_date​ is the last recorded edit timestamp
● write_uid​ is the user that last edited the record

Another special column that can be added to a model is ​active​ . It should be a Boolean flag allowing for
mark records as inactive. Its definition looks like this:
active = fields.Boolean('Active', default=True)
Fields
Date and Datetime
Date​ stores date values. The ORM handles them in the string format, but they are stored in the database
as dates. The format used is defined in openerp.fields.DATE_FORMAT .

For Date , we have the following:


● fields.Date.from_string(string_value)​ parses the string into a date object.
● fields.Date.to_string(date_value)​ represents the Date object as a string.
● fields.Date.today()​ returns the current day in string format.
● fields.Date.context_today(record)​ returns the current day in string format according to the
timezone of the record's (or recordset) context.

Example:
today_str = fields.Date.context_today()

Datetime​ for date-time values. They are stored in the database in a naive date time, in UTC time. The
ORM represents them as a string and also in UTC time. The format used is defined in
openerp.fields.DATETIME_FORMAT .

For Datetime , we have the following:


● fields.Datetime.from_string(string_value)​ parses the string into a datetime object.
● fields.Datetime.to_string(datetime_value)​ represents the datetime object as a string.
● fields.Datetime.now()​ returns the current day and time in string format. This is appropriate to use
for default values.
● fields.Datetime.context_timestamp(record, value)​ converts a value naive date-time into a
timezone-aware date-time using the timezone in the context of record . This is not suitable for
default values.

Example:
from odoo import models, fields, api
from datetime import timedelta as td

class LibraryBook(models.Model):
_name = 'library.book'
_description = 'Library Book'

date_release = fields.Date('Release Date')


age_days = fields.Float(string='Days Since Release',
compute='_compute_age',
inverse='_inverse_age',
search='_search_age',
store=False,
compute_sudo=False)

@api.depends('date_release')
def _compute_age(self):
today = fields.Date.from_string(fields.Date.today())
for book in self.filtered('date_release'):
delta = today - fields.Date.from_string(book.date_release)
book.age_days = delta.days

def _inverse_age(self):
today = fields.Date.from_string(fields.Date.today())
for book in self.filtered('date_release'):
d = today - td(days=book.age_days)
book.date_release = fields.Date.to_string(d)

def _search_age(self,operator,value):
today = fields.Date.from_string(fields.Date.today())
value_days = td(days=value)
value_date = fields.Date.to_string(today - value_days)
return [('date_release',operator,value_date)]

fields.Date.today() and fields.Datetime.now() returns wrong values


Lưu ý: fields.Date.today() và fields.Datetime.now() trã về giá trị ngày giờ của lần server restart​ trước đó.

Để lấy thời gian hiện tại set vào default fields:

from datetime import datetime

date_start = fields.Date(string='Date start',


default=lambda r: datetime.strftime(datetime.now(),'%Y-%m-%d')​)
time_start = fields.Datetime(string='Time start',
default=lambda r: datetime.strftime(datetime.now(),'%Y-%m-%d %H:%M:%S')​)

One2many
Many2one

Related field from One2many:

class CastingTree(models.Model):
_name = 'casting.tree'
_description = 'Casting Tree'
...
# CASTING fields
ca_lot_id = fields.Many2one(comodel_name='production.lot',string='Casting Lot',readonly=True)
ca_lot_line_ids = fields.One2many(related='ca_lot_id.line_ids',string='Casting Lines')

Field Attributes
Attributes

Attributes Examples Description

string

size

translate

default default=_compute_default is the default value. It can also be a function that is


used to calculate the default value. For example,
default=_compute_default , where
_compute_default is a method defined on the
model before the field definition.

help

groups groups='base.group_user' groups makes the field available only to some


security groups. It is a string containing a
comma-separated list of XML IDs for security
groups.

states states={‘draft’:[ allows the user interface to dynamically set the


(‘readonly’,False), value for the readonly, required , and invisible
(‘required’,True)]} attributes, depending on the value of the state
field.
Therefore, it requires a state field to exist and be
used in the form view (even if it is
invisible).

copy copy=False copy flags if the field value is copied when the
record is duplicated. By default, it is True for
non-relational and Many2one fields and False for
One2many and computed fields.

index index=True when set to True , makes for the creation of a


database index for the field, allowing faster
searches. It replaces the deprecated select=1
attribute.

readonly readonly=True The readonly flag makes the field read-only by


default in the user interface.

required required=True The required flag makes the field mandatory by


default in the user interface.

sanitize The sanitize flag is used by HTML fields and strips


its content from potentially
insecure tags.

strip_style strip_style is also an HTML field attribute and has


the sanitization to also remove
style elements.

company_dependent The company_dependent flag makes the field


store different values per company. It
replaces the deprecated Property field type.

Example​: Use Employee editable only on Draft

class EmployeePayroll(models.Model):
_name = 'hr.employee.payroll'
_description = 'Employee Payroll'

name = fields.Char(string='Name',readonly=True)
​state​ = fields.Selection([('draft','Draft'),('done','Done')],string='State',default='draft')

employee_id = fields.Many2one(comodel_name='hr.employee',string='Employee',required=True,
​readonly=True,states={'draft':[('readonly',False)]}​)

Relation field attributes

ondelete
The ​ondelete​ attribute determines what happens when the related record is deleted. For example, what
happens to Books when their Publisher record is deleted? The default is 'set
null' , setting an empty value on the field. It can also be 'restrict' , which prevents the related record from
being deleted, or 'cascade' , which causes the linked record to also be deleted.
● ‘Set null’: res.partner A deleted → Library.book B :: publisher_id = null
● ‘Restrict’ : res.partner A gonna deleted → NO!!! NOT ALLOW
● ‘Cascade’ : res.partner A deleted → All his published Library.books will be deleted

context and domain


context and domain are also valid for the other relational fields. They are mostly meaningful on the ​client
side​ and at the model level act just as default values to be used in the client-side views.
● context​ adds variables to the client context when clicking through the field to the related record's
view. We can, for example, use it to set default values on that view.
● domain​ is a search filter used to limit the list of related records available for selection when
choosing a value for our field.

Computed Fields

Example:
from odoo import models, fields, api
from datetime import timedelta as td

class LibraryBook(models.Model):
_name = 'library.book'
_description = 'Library Book'

date_release = fields.Date('Release Date')


age_days = fields.Float(string='Days Since Release',
compute='_compute_age',
inverse='_inverse_age',
search='_search_age',
store=False,
compute_sudo=False​)

@api.​depends​('date_release')
def ​_compute_age​(self):
today = fields.Date.from_string(fields.Date.today())
for book in self.filtered('date_release'):
delta = today - fields.Date.from_string(book.date_release)
book.age_days = delta.days

def ​_inverse_age​(self):
today = fields.Date.from_string(fields.Date.today())
for book in self.filtered('date_release'):
d = today - td(days=book.age_days)
book.date_release = fields.Date.to_string(d)

def ​_search_age​(self,operator,value):
today = fields.Date.from_string(fields.Date.today())
value_days = td(days=value)
value_date = fields.Date.to_string(today - value_days)
return [('date_release',operator,value_date)]

The optional ​store=True​ flag makes the field stored in the database. In this case, after being computed,
the field values are stored in the database, and from there on, they are retrieved like regular fields instead
of being recomputed at runtime.
Thanks to the ​@api.depends​ decorator, the ORM will know when these stored values need to be
recomputed and updated.

The ​compute_sudo=True​ flag is to be used in those cases where the computations need to be done with
elevated privileges. This can be the case when the computation needs to use data that may not be
accessible to the end user.

Search
Example:

class WorkorderInherit(models.Model):
_inherit = 'mrp.workorder'

item_work_ids = fields.One2many('item.work','workorder_id',string='Item works',readonly=True)


qty_remain = fields.Float(string='Remain Qty',compute='_get_qty_remain',​search='_search_qty_remain'​,
digits=dp.get_precision('Product Unit of Measure'))

def ​_search_qty_remain​(self, operator, value):


ids = []
if operator == '>':
ids = self.env['mrp.workorder'].search([('state','not in',['done','cancel'])])
.filtered(lambda wo: wo.qty_remain > float(value)).mapped('id')
return [('id', 'in', ids)]
@api.depends('item_work_ids')
@api.multi
def _get_qty_remain(self):
for r in self:
r.qty_remain = sum(1 for iwo in r.item_work_ids if iwo.state not in ['done','cancel'])
return True

Related Fields

Example:

class LibraryBook(models.Model):
_name = 'library.book'
...
publisher_id = fields.Many2one('res.partner',string='Publisher',
ondelete='set null',context={},domain=[])
publisher_city = fields.Char('Publisher City',related='publisher_id.city')

Related fields are in fact ​computed fields​. They just provide a convenient shortcut syntax to read field
values from related models. As a computed field, this means that the store attribute is also available to
them. As a shortcut, they also have all the attributes from the referenced field, such as ​name​ ,
translatable​ , ​required​ , and so on.
Additionally, they support a ​related_sudo​ flag similar to compute_sudo ; when set to True , the field chain
is traversed without checking user access rights.

Parent Child relation

Example:
class BookCategory(models.Model):
_name = 'library.book.category'
name = fields.Char('Category')
parent_id​ = fields.Many2one('library.book.category',
string='Parent Category',
ondelete='restrict',index=True )
child_ids​ = fields.One2many('library.book.category','parent_id',
string='Child Categories')
_parent_store​ = True
parent_left​ = fields.Integer(index=True)
parent_right​ = fields.Integer(index=True)
@api.constrains('parent_id')
def _check_hierarchy(self):
if not self.​_check_recursion​():
raise models.ValidationError('Error ! You cannot create recursive categories.')

We activate the special support for hierarchies. This is useful for high-read but low-write instructions, since
it brings faster data browsing at the expense of costlier write operations. It is done by adding two helper
fields, ​parent_left​ and ​parent_right​ , and setting the model attribute to ​_parent_store=True​ . When this
attribute is enabled, the two helper fields will be used to store data in searches in the hierarchic tree.
By default, it is assumed that the field for the record's Parent is called ​parent_id​ , but a different name can
be used. In that case, the correct field name should be indicated using the additional model attribute
_parent_name​ . The default is as follows:
_parent_name = 'parent_id'

Constraint
Database constraints
Database level constraints are limited to the constraints supported by PostgreSQL. The most commonly
used are the ​UNIQUE​ constraints, but ​CHECK​ and ​EXCLUDE​ constraints can also be used.

Example:
_sql_constraints = [
('name_uniq', 'UNIQUE (name)', 'Book title must be unique.')
]
As mentioned earlier, other database table constraints can be used. Note that column constraints, such as
NOT NULL​ , can't be added this way. For more information on PostgreSQL constraints in general and
table constraints in particular, take a look at ​http://www.postgresql.org/docs/9.4/static/ddl-constraints.html
.

Server python constraints

Example:
from openerp import api
class LibraryBook(models.Model):
#…
@api.constrains('date_release')
def _check_release_date(self):
for r in self:
if r.date_release > fields.Date.today():
raise models.ValidationError('Release date must be in the past')

Delegation Inheritance

Example:

class LibraryMember(models.Model):
_name = 'library.member'
_inherits = {'res.partner':'partner_id'}

partner_id = fields.Many2one('res.partner',ondelete='cascade')
date_start = fields.Date('Member Since')
date_end = fields.Date('Termination Date')
member_number = fields.Char()

To better understand how it works, let's look at what happens on the database level when we create a
new Member:
● A new record is created in the ​res_partner​ table
● A new record is created in the ​library_member​ table
● The ​partner_id​ field of the ​library_member​ table is set to the id of the ​res_partner​ record that is
created for it

It's important to note that delegation inheritance ​only works for fields and not for methods​.

Create
Create returns a ​model object​ .

Example

@api.model
def create(self,values):
res = super(CastingItem,self).​create​(values)
print '\n\n =====> CastingItem create',res,values
res.option_id._calc_remain_qty()
return res
Write
Write returns True / False

Example:
@api.multi
def write(self,values):
res = super(CastingItem,self).​write​(values)
print '\n\n =====> CastingItem write',res,values
if 'quantity' in values:
self.option_id._calc_remain_qty()
return res

Unlink
Unlink returns True / False

Example:
@api.multi
def unlink(self):
option = self.option_id
res = super(CastingItem,self).​unlink​()
print '\n\n===> CastingItem unlink',res,option
if option:
option._calc_remain_qty()
return res

Controller Inherit

Example: inherit Website_form submission function

from odoo import ​addons


from odoo import http
from odoo.http import request
class FeosSurveyExtension(​addons.website_form.controllers.main.WebsiteForm​):

@http.route()
def website_form(self,model_name,**kwargs):
if model_name != 'hr.applicant':
return super(FeosSurveyExtension, self).website_form(model_name,**kwargs)
job_id = int(kwargs['job_id'])
survey_id = int(kwargs['survey_id'])# Return errors messages to webpage

More pro:

odoo/addons/website_form/controllers/main.py

class WebsiteForm(http.Controller):

# Check and insert values from the form on the model <model>
@http.route('/website_form/<string:model_name>', type='http', auth="public", methods=['POST'],
website=True)
def website_form(self, model_name, **kwargs):
model_record = request.env['ir.model'].search([('model', '=', model_name), ('website_form_access',
'=', True)])
if not model_record:
return json.dumps(False)

from odoo.addons.website_form.controllers.main import WebsiteForm

class FeosSurveyExtension(WebsiteForm):
@http.route()
def website_form(self,model_name,**kwargs):
if model_name != 'hr.applicant':
return super(FeosSurveyExtension, self).website_form(model_name,**kwargs)
job_id = int(kwargs['job_id'])
survey_id = int(kwargs['survey_id'])# Return errors messages to webpage

http_res = super(FeosSurveyExtension, self).website_form(model_name,**kwargs)
return http_res
Javascript
Call order of functions

1. Init
2. willStart
3.

Function
Predefined functions
name_get
This function is called to compute display_name
Example:
def name_get(self):
result = []
for r in self:
result.append( (r.id, u"%s (%s)" % (r.name, r.date_release)) )
return result

_name_search

@api.model
def _name_search(self, name='', args=None, operator='ilike', limit=100,name_get_uid=None):
args = [] if args in None else args.copy()
if not (name == '' and operator=='ilike'):
args += ['|','|',
('name',operator,name),
('isbn',operator,name),
('author_ids.name',operator,name),
]
return super(LibraryBook,self)._name_search(
name='',args=args,operator='ilike',
limit=limit,name_get_uid=name_get_uid)

Create

Example:

class LibraryBook(models.Model):
...
@api.model
@api.returns('self',lambda rec: rec.id)
def create(self,values):
if not self.user_has_groups('library.group_library_manager'):
if 'manager_remarks' in values:
raise exceptions.UserError('You are not allowed to modify'
'manager_remarks')
return super(LibraryBook,self).create(values)

Write

Example:

@api.multi
def write(self,values):
if not self.user_has_groups('library.group_library_manager'):
if 'manager_remarks' in values:
raise exceptions.UserError('You are not allowed to modify',
'manager_remarks')
return ​super(LibraryBook,self).write(values)
Field_get

Example:

@api.model
def fields_get(self,allfields=None,write_access=True,attributes=None):
fields = super(LibraryBook,self).fields_get(
allfields=allfields,
write_access=write_access,
attributes=attributes
)
if not self,user_has_groups('library.group_library_manager'):
if 'manager_remarks' in fields:
fields['manager_remarks']['readonly'] = True

fields_view_get

Example: stock/models/product.py

class Product(models.Model):
_inherit = "product.product"

...
@api.model
def ​fields_view_get​(self, view_id=None, view_type='form', toolbar=False, submenu=False):
res = super(Product, self).fields_view_get(view_id=view_id, view_type=view_type, toolbar=toolbar,
submenu=submenu)
if self._context.get('location') and isinstance(self._context['location'], (int, long)):
location = self.env['stock.location'].browse(self._context['location'])
fields = res.get('fields')
if fields:
if location.usage == 'supplier':
if fields.get('virtual_available'):
res['fields']['virtual_available']['string'] = _('Future Receipts')
if fields.get('qty_available'):
res['fields']['qty_available']['string'] = _('Received Qty')
elif location.usage == 'internal':
if fields.get('virtual_available'):
res['fields']['virtual_available']['string'] = _('Forecasted Quantity')
elif location.usage == 'customer':
if fields.get('virtual_available'):
res['fields']['virtual_available']['string'] = _('Future Deliveries')
if fields.get('qty_available'):
res['fields']['qty_available']['string'] = _('Delivered Qty')
elif location.usage == 'inventory':
if fields.get('virtual_available'):
res['fields']['virtual_available']['string'] = _('Future P&L')
if fields.get('qty_available'):
res['fields']['qty_available']['string'] = _('P&L Qty')
elif location.usage == 'procurement':
if fields.get('virtual_available'):
res['fields']['virtual_available']['string'] = _('Future Qty')
if fields.get('qty_available'):
res['fields']['qty_available']['string'] = _('Unplanned Qty')
elif location.usage == 'production':
if fields.get('virtual_available'):
res['fields']['virtual_available']['string'] = _('Future Productions')
if fields.get('qty_available'):
res['fields']['qty_available']['string'] = _('Produced Qty')
return res

Example 2:
Python code:
class ItemWork(models.Model):
_name = 'item.work'

...

@api.model
def ​fields_view_get​(self, view_id=None, view_type='form', toolbar=False, submenu=False):
wcid = self._context.get('workcenter_id')
if view_type == 'tree':
if wcid == self.env.ref('jewelry_base.wc_stone_02').id:
view_id = self.env.ref('jewelry.view_item_work_lot_stone2_editable_tree').id
else:
view_id = self.env.ref('jewelry.view_item_work_lot_tree').id
res = super(ItemWork, self).fields_view_get(view_id=view_id, view_type=view_type, toolbar=toolbar,
submenu=submenu)
return res

Views.xml
<field name="item_work_ids" colspan="2" nolabel="1"
attrs="{'​readonly​':[('state','not in',['ready','progress'])]}"
context="{'tree_view_ref':'jewelry.view_item_work_lot_stone2_editable_tree}" />

<record id="view_item_work_lot_stone2_editable_tree" model="ir.ui.view">


<field name="name">Item Work</field>
<field name="model">item.work</field>
<field name="arch" type="xml">
<tree create="0" delete="0" editable="bottom" decoration-warning="item_scrap_id != False">
<field name="product_id"/>
<field name="color_id"/>
<field name="size_id"/>
<field name="state"/>
<field name="stone1_weight" sum="Stone1 weight"/>
<field name="stone2_weight" sum="Stone2 weight"/>
<field name="stone_weight" sum="Stone weight"/>
<field name="item_scrap_id" invisible="1"/>
</tree>
</field>
</record>

name_search

Example​: import country.state csv file, we don’t need to indicate country id, but use country code instead

File: res.country.state.csv

"id","country_id:id","name","code"
state_au_1,​au​,"Australian Capital Territory","ACT"
state_au_2,​au​,"New South Wales","NSW"
state_au_3,​au​,"Northern Territory","NT"
state_au_4,​au​,"Queensland","QLD"
state_au_5,​au​,"South Australia","SA"
state_us_1,​us​,"Alabama","AL"
state_us_2,​us​,"Alaska","AK"
state_us_3,​us​,"Arizona","AZ"

File: res_country.py
@api.model
def ​location_name_search​(self, name='', args=None, operator='ilike', limit=100):
if args is None:
args = []

records = self.browse()
if len(name) == 2:
records = self.search([('code', 'ilike', name)] + args, limit=limit)

search_domain = [('name', operator, name)]


if records:
search_domain.append(('id', 'not in', records.ids))
records += self.search(search_domain + args, limit=limit)

# the field 'display_name' calls name_get() to get its value


return [(record.id, record.display_name) for record in records]

class Country(models.Model):
_name = 'res.country'
_description = 'Country'
_order = 'name'

name = fields.Char(string='Country Name', required=True, translate=True, help='The full name of the


country.')
code = fields.Char(string='Country Code', size=2,
help='The ISO country code in two chars. \nYou can use this field for quick search.')

state_ids = fields.One2many('res.country.state', 'country_id', string='States')

_sql_constraints = [
('name_uniq', 'unique (name)',
'The name of the country must be unique !'),
('code_uniq', 'unique (code)',
'The code of the country must be unique !')
]

name_search = location_name_search

class CountryState(models.Model):
_description = "Country state"
_name = 'res.country.state'
_order = 'code'

country_id = fields.Many2one('res.country', string='Country', required=True)


name = fields.Char(string='State Name', required=True,
help='Administrative divisions of a country. E.g. Fed. State, Departement, Canton')
code = fields.Char(string='State Code', help='The state code.', required=True)

​ name_search = location_name_search

_sql_constraints = [
('name_code_uniq', 'unique(country_id, code)', 'The code of the state must be unique by country !')
]

Decorator

Hiding methods from the RPC interface


In the old API, methods with a name prefixed by an underscore are not exposed through the
RPC interface and are therefore not callable by the web client. This is still the case with the
new API, which also offers another way to make a method unavailable to the RPC interface;
if you don't put an ​@api.model​ or ​@api.multi​ decorator on it , then the method will neither be callable by
extensions of the model using the traditional API nor via RPC

@api.one​ (deprecated)
In Odoo 9.0, this decorator is ​deprecated​ because its behavior can be confusing—at first glance, and
knowing of ​@api.multi​ , it looks like this decorator allows the method to be called only on recordsets of
size 1 , but it does not. When it comes to recordset length, ​@api.one​ is similar to @
​ api.multi​ , but it does
a for loop on the recordset outside the method and aggregates the returned value of each iteration of the
loop in a list, which is returned to the caller. Avoid using it in your code.

@api.multi
@api.model

@api.returns
This decorator maps the returned value from the new API to the old API, which is expected by the RPC
protocol. In this case, the RPC calls to create expect the database id for the new record to be created, so
we pass the @api.returns decorator an anonymous function, which
fetches the id from the new record returned by our implementation. It is also needed if you want to extend
the copy() method. Do not forget it when extending these methods if the base implementation uses the old
API or you will crash with hard to interpret messages.

@api.onchange
Lưu ý: api.onchange chỉ được gọi khi field đó được khai báo trong views xml. Có thể dùng invisible="1" để
ẩn field đi.

Notice: if want to set other field values, set them directly in the onchange function. Should only return dict
of domain or return {}

Example 1:

class Employee(models.Model):
_inherit = "hr.employee"

deliverable_employee = fields.Boolean(string='Deliverable',
help='True if employee is able to deliver orders',default=False)
is_delivering = fields.Boolean(string='Is Delivering',
help='True if employee is busy delivering',default=False)
delivery_orders = fields.One2many(comodel_name='delivery.order',
inverse_name='employee_id',
string='Current Deliveries',domain=[('state','not in',['done','cancel'])])

@api.onchange('department_id')
def _onchange_deparment(self):
if self.department_id.id == self.env.ref('feos_delivery.delivery_department').id:
self.deliverable_employee = True
return {}
Example 2:

class ​mrp_machines​(models.Model):
_name = 'mrp.machines'
name = fields.Char('Name')

description = fields.Text('Description')
image = fields.Binary('Image')
bom_ids = fields.Many2many(comodel_name='mrp.bom', string='BOMs')
bom_id = fields.Many2one(comodel_name='mrp.bom', string='Bill of material')
# … other fields

# @api.depends('bom_id','counting_workcenter_id')
# @api.one
​@api.onchange('bom_id')
def _onchange_bom_id(self):
print '_onchange_bom_id',self.bom_id
res = {}
if(self.bom_id.routing_id):
wc_ids = []
wc_id = False
for wl in self.bom_id.routing_id.workcenter_lines:
wc_ids.append(wl.workcenter_id.id)
if (not wc_id) and (wl.workcenter_id.resource_type == 'user'):
wc_id = wl.workcenter_id.id
self.counting_workcenter_id = wl.workcenter_id
print 'wc_ids',wc_ids
res = {
​'warning'​:{
'title':'onchange bom id',
'message': 'You have change the BOM ID'
},
​'domain'​: {
'counting_workcenter_id': [('id','in',wc_ids)]
},
​'value'​:{
'counting_workcenter_id': wc_id
}
}
return res

Execute SQL
The object in self.env.cr is a thin wrapper around a psycopg2 cursor. The following methods are the ones
you will want to use most of the time:

● execute(query, params)​ : This executes the SQL query with the parameters marked as %s in the
query substituted with the values in params, which is a tuple
Warning​: never do the substitution yourself, as this can make the code vulnerable to ​SQL
injections​.
● fetchone()​ : This returns one row from the database, wrapped in a tuple (even if there is only one
column selected by the query)
● fetchall()​ : This returns all the rows from the database as a list of tuples
● fetchalldict()​ : This returns all the rows from the database as a list of dictionaries mapping column
names to values

class ResPartner(models.Model):
_inherit = 'res.partner'

@api.model:
def partners_by_country(self):
sql = ('SELECT country_id, ​array_agg​(id) '
'FROM res_partner '
'WHERE active=true AND country_id IS NOT NULL '
'GROUP BY country_id')
self.env.cr.execute(sql)
country_model = self.env['res.country']
result = {}
for country_id, partner_ids in ​self.env.cr.fetchall()​:
country = country_model.browse(country_id)
partners = self.search(
[('id', 'in', tuple(partner_ids))]
)
result[country] = partners
return result

we declare an SQL ​SELECT​ query. It uses the id field and the ​country_id​ foreign key, which refers to the
res_country​ table. We use a ​GROUP BY​ statement so that the database does the grouping by
country_id​ for us, and the ​array_agg​ aggregation function. This is a very useful PostgreSQL extension to
SQL that puts all the values for the group in an array, which Python maps to a list.
Actions
ir.actions.act_window

Menu action

<record id="workorder_tree_view" model="ir.ui.view">


<field name="name">Work Orders</field>
<field name="model">mrp.workorder</field>
<field name="arch" type="xml">
<tree>
<field name="partner_id"/>
<field name="name"/>
<field name="state"/>
<field name="product_id"/>
<field name="qty_produced"/>
<field name="qty_remain"/>
<field name="date_planned_start"/>
</tree>
</field>
</record>

<record id="workorder_responsible_search_view" model="ir.ui.view">


<field name="name">Responsible Workorders Search</field>
<field name="model">mrp.workorder</field>
<field name="arch" type="xml">
<search>
<field name="name" />
<field name="workcenter_id" />
<field name="user_id" />
<field name="date_start" />
​<filter name="my_todos" string="My Todos" domain="[('user_id','=',uid),('state','not
in',['done','cancel'])]"/>
<group string="Group By">
<filter name="by_workcenter" string="Workcenter"
context="{'group_by':'workcenter_id'}" />
<filter name="by_user" string="Responsible" context="{'group_by':'user_id'}" />
</group>
</search>
</field>
</record>
<record id="action_stone1_workorder_mgt" model="​ir.actions.act_window​">
<field name="name">Wax Workorders</field>
<field name="res_model">mrp.workorder</field>
<field name="view_type">form</field>
<field name="view_mode">tree,form,kanban</field>
​<field name="​view_id​" ref="jewelry.workorder_tree_view"></field>
<field name="​search_view_id​" ref="jewelry.workorder_responsible_search_view"/>
<field name="​domain​" eval="[('workcenter_id','in',[ ref('jewelry_base.wc_wax_injection') ] ), ('state','not in',
['done','cancel'] ), ('qty_remain','&gt;',0),('master_production','=',True) ]" />
<field name="​context​">{
'​search_default_my_todos​':1,
'​group_by​':['partner_id','sale_id','production_id']}
</field>

</record>

Return to client
Client action return from Server go to object with ID
Action return to client to goto Res.partner with id = 1
return {
"action":{
"type": "ir.actions.act_window",
"​res_model​": 'res.partner',
"views": [[False, "form"]],
"target": "current", ​# ​‘inline’​ for edit mode
"​res_id​": 1,
"context":{'form_view_initial_mode': 'edit'}
}
}
Example 1​: Wizard button select run codes but not close Wizard
@api.multi
def action_select(self):
self.ensure_one()
...
return {
"type": "​ir.actions.do_nothing​",
}

Example 2​: Wizard submit create new object and goto that object form

class CastingTreeWizard(models.TransientModel):
_name = 'casting.tree.wizard'

...

@api.multi
def button_cast(self):
...
return {
"type": "ir.actions.act_window",
"res_model": 'casting.batch',
"views": [[False, "form"]],
"target": "current", # ‘inline’ for edit mode
"res_id": batch.id,
"context":{'form_view_initial_mode': 'edit'}
}

Close Wizard
@api.multi
def action_done(self):
return {'type': '​ir.actions.act_window_close​'}

Context in action
<record id="open_ask_holidays" model="ir.actions.act_window">
<field name="name">Leaves Request</field>
<field name="res_model">hr.holidays</field>
<field name="view_type">form</field>
<field name="view_id" ref="edit_holiday_new"/>
<field name="​context​">​{
'default_type': 'remove',
'search_default_my_leaves': 1,
'needaction_menu_ref':
[
'hr_holidays.menu_open_company_allocation',
]
}​</field>
<field name="domain">[('type','=','remove')]</field>
<field name="search_view_id" ref="view_hr_holidays_filter"/>
<field name="help" type="html">
<p class="oe_view_nocontent_create">
Click to create a new leave request.
</p><p>
Once you have recorded your leave request, it will be sent
to a manager for validation. Be sure to set the right leave
type (recuperation, legal holidays, sickness) and the exact
number of open days related to your leave.
</p>
</field>
</record>

'Needaction_menu_ref' ​? :
https://www.odoo.com/fr_FR/forum/aide-1/question/how-to-display-needaction-count-only-in-one-menu-90
925

ORM
Create new record
In the dictionary:
● Text​ field values are given with Python strings (preferably Unicode strings).
● Float​ and Integer field values are given using Python floats or integers.
● Boolean​ field values are given, preferably using Python booleans or integer.
● Date​ (resp. Datetime ) field values are given as Python strings. Use ​fields.Date.to_string()​ (resp.
fields.Datetime.to_string()​ ) to convert a Python datetime.date (resp. datetime.datetime ) object
to the expected format.
● Binary​ field values are passed as a Base64 encoded string. The ​base64​ module from the Python
standard library provides methods such as encodestring(s) to encode a string in Base64.
● Many2one​ field values are given with an integer, which has to be the database ID of the related
record.
● One2many and Many2many fields use a special syntax. The value is a list containing tuples of
three elements, as follows:

Tuple Effect

( 0, 0, dict_val ) Create a new record that will be related to the main record

(4, id) adds an existing record of id ​id​ to the set. Can not be used on
One2many​.

( 6, 0, id_list ) Create a relation between the record being created and existing
records, whose IDs are in the Python list id_list
Caution: When used on a One2many, this will remove the records
from any previous relation

Example: To create res.partner with child contact:


def demo_create(self):
today_str = fields.Date.context_today()
val1 = {'name': u'Eric Idle',
'email': ​u'eric.idle@example.com​',
'date': today_str}
val2 = {'name': u'John Cleese',
'email': ​u'john.cleese@example.com​',
'date': today_str}
partner_val = {
'name': u'Flying Circus',
'email': ​u'm.python@example.com​' ,
'date': today_str,
'is_company': True,
'child_ids': [
(0, 0, val1),
(0, 0, val2),
]
}
record = self.env['res.partner'].create(partner_val)
Updating values of recordset records

Example:
@api.model
def add_contacts(self, partner, contacts):
partner.ensure_one()
if contacts:
today = fields.Date.context_today()
partner.​update​(
{'date': today,
'child_ids': partner.child_ids | contacts}
)

Write values
write()​ method, passing a dictionary mapping field names to the values you want to set.
This method ​works for recordsets of arbitrary size and will update all records​ with the specified values in
one single​ database operation​ when the two previous options perform one database call per record and
per field. However, it has some limitations:
● It does ​not work if the records are not yet present in the database
● It requires using a ​special format​ when writing relational fields, similar to the one used by the
create() method

Tuple Effect

(0, 0, dict_val) adds a new record created from the provided ​dict_val​ dict.

(1, id, dict_val) updates an existing record of id id with the values in ​dict_val​. Can not be
used in ​create​()

(2, id) removes the record of id ​id​ from the set, then deletes it (from the
database). Can not be used in ​create​().

(3, id) removes the record of id ​id​ from the set, but does not delete it. Can not be
used on ​One2many​. Can not be used in ​create​().

(4, id) adds an existing record of id ​id​ to the set. Can not be used on
One2many​.
(5, ) removes all records from the set, equivalent to using the command ​3​ on
every record explicitly. Can not be used on ​One2many​. Can not be used
in ​create​().

(6, 0, ids) replaces all existing records in the set by the ​ids​ list, quivalent to using
the command ​5​ followed by a command ​4​ for each ​id​ in ​ids​.

Example: Append current stage to list of successfully sent stages .

class HrApplicant(models.Model):
_inherit = 'hr.applicant'

mailed_stage_ids = fields.Many2many('hr.recruitment.stage',string='Mailed stages',


relation='mailed_applicant_stage_relation',column1='applicant_id',column2='stage_id')

@api.multi
@api.depends('stage_id')
def log_sent_mail_stages(self):
for r in self:
r.mailed_stage_ids =​ [(4,r.stage_id.id)]

Searching
Example:
self.env[‘res.partner’].search(domain)

Keyword arguments are supported:


● offset=N​ : This is used to skip the N first records that match the query. This can be used together
with limit to implement pagination or to reduce memory consumption when processing a very large
number of records. It defaults to 0 .
● limit=N​ : return at most N records. By default, there is no limit.
● order=sort_specification​ : This is used to force the order on the returned recordset. By default,
the order is given by the _order attribute of the model class.
● count=boolean​ : If True , this returns the number of records instead of the recordset. It defaults to
False .
Example:
self.env[‘res.partner’].search(domain,offset=1,limit=80,order=’name’)

We recommend using the ​search_count(domain)​ method rather than ​search(domain, count=True)​, as


the name of the method conveys the behavior in a much clearer way; both will give the same result.

If for some reason you find yourself writing raw SQL queries to find record IDs, be sure to
use ​self.env[' record.model '].search([ ('id', 'in', tuple(ids)) ]).ids​ after retrieving the IDs to make sure that
security rules​ are applied. This is especially important in ​multicompany​ Odoo instances where record
rules are used to ensure proper discrimination between companies.

Combining Recordsets
Works on the same model:

Operator Action performed

R1 + R2 This returns a new recordset containing the records from R1


followed by the records from R2. This can generate duplicate
records in the recordset.

R1 - R2 This returns a new recordset consisting of the records from R1,


which are not in R2. The order is preserved.

R1 & R2 This returns a new recordset with all the records that belong to
both R1 and R2 (intersection of recordsets). The order is not
preserved here.

R1 == R2 True if both recordsets contain the same records.

R1 <= R2 True if all records in R1 are also in R2. Both syntaxes are
R1 in R2 equivalent.

R1 >= R2 True if all records in R2 are also in R1. Both syntaxes are
R2 in R1 equivalent.

R1 != R2 True if R1 and R2 do not contain the same records.

R1 | R2 This returns a new recordset with the records belonging to either


R1 or R2 (union of recordsets). The order is not preserved, but
there are no duplicates.
Filter

Example 1:

def predicate(partner):
If partner.email:
return True
return False

@api.model
def parters_with_email(self, partners):
return partners.​filter​(predicate)
Example 2:

@api.model
def partners_with_email(self,partners):
return partners.​filter​(​lambda​ p: p.email)

Keep in mind that filter() operates in the memory → not good for performance

Mapped

Example:

@api.model
def get_email_addresses(self, partner):
partner.ensure_one()
return partner.​mapped(‘child_ids.email’)

@api.model
def get_companies(self, partners):
return partners.​mapped(‘parent_id’)

When using ​mapped()​ , keep in mind that it operates in the ​memory​ inside the Odoo server by repeatedly
traversing relations and therefore making SQL queries, which may ​not be efficient​; however, the code is
terse and expressive.
Views
Field
State

Example:
<header>
<field name="state" widget="statusbar" clickable="True"
statusbar_visible="draft,ready,metal_assignment,casting,cutting,done"/>
</header>

Domain
Ref:
● https://stackoverflow.com/questions/45506255/odoo-multiple-condition-in-domain-error
● https://www.freeformatter.com/html-entities.html

<field name="casting_tree_ids" widget="many2many"


domain="[('metal_type_id','=',metal_type_id),('state','in',['ready'])]" />

<field name="number_of_days_temp" position="attributes">


<attribute name="attrs">
{'readonly':[ '|',
('state','not in',['draft','confirm']),
'​&#38;​',
('allocation_type','=','accumulation'), ('type','=','add')]}
</attribute>
</field>

Domain ID in One2many
Domain id in One2many field (cannot use for Many2many in Wizard):
<field name="color_ids" invisible="1" />
<field name="color_id" domain="[('id','in',​color_ids[0][2]​)]" />

Options
no_create
<field name="size_id" ​options="{'no_create': True}"​/>

no_quick_create

no_create_edit

<field name="operation_id" groups="mrp.group_mrp_routings"


domain="[('routing_id', '=', parent.routing_id)]"
options="{​'no_quick_create'​:True,​'no_create_edit'​:True}" />

reload_on_button and always_reload

<field name="invoice_ids" ​options="{'reload_on_button': true}"​>


<tree>
<field name="number"/>
<field name="state"/>
<field name="amount_total"/>
<field name="residual"/>
<field name="date_invoice"/>
<button name="print_receipt" type="object" icon="fa-print"/>
</tree>
</field>

Dùng options dưới đây hiệu quả hơn với Odoo 11


options="{'always_reload':True}"

Context

default values
Usage: ​default_​field_name

<field name="work_item_ids" nolabel="1" colspan="2"


context="{
'​default_tree_line_id​':id,
'​default_workorder_id​':workorder_id}" />

Tree_view_ref & Form_view_ref


<field name="work_item_ids" nolabel="1" colspan="2"
context="{
'default_tree_line_id':id,
'default_workorder_id':workorder_id,
'​tree_view_ref​':'jewelry.view_work_line_item_simple_tree',
'​form_view_ref​':'jewelry.view_work_line_item_casting_form'}" />

Hide tree view column based on context


Ví dụ: ẩn thông tin cột Task​ nếu khâu đó không quản lý theo Employees​.

<form>

<field name="management_type" />
<field name="item_work_ids" colspan="2" nolabel="1"
context="{
'tree_view_ref':'jewelry.view_item_work_lot_tree',
'management_type':management_type​}"
/>
...
</form>

<record id="view_item_work_lot_tree" model="ir.ui.view">


<field name="name">Item Work</field>
<field name="model">item.work</field>
<field name="arch" type="xml">
<tree create="0" delete="0">
<field name="id" groups="base.group_no_one"/>
<field name="lot_id" groups="base.group_no_one"/>
<field name="product_id"/>
<field name="color_id"/>
<field name="size_id"/>
<field name="state"/>
<field name="mo_id"/>
<field name="workcenter_id" groups="base.group_no_one"/>
​<field name="task_id" invisible="context.get('management_type') != 'employees'"/>
<field name="stone_weight" sum="Stone weight"/>
</tree>
</field>
</record>
Attributes

Invisible
<field name="workcenter_id" options="{'no_create':True}"
attrs="{​'invisible'​:[('move_type','in',['lot','state'])] }"/>

Required

<field name="state" attrs="{​'required'​:[('move_type','=','state')]}"/>

Widgets

many2many_tags

one2many_list

selection

progressbar

selection
statusbar

handle

monetary

mail_thread

statinfo

contact

html

mail_followers

url

radio

email

one2many

many2manyattendee

priority

integer

sparkline_bar

many2many_binary

image

many2many_kanban

char_domain

gauge

float_time
Others
separator
<separator string="Application Summary"/>
<field name="description" placeholder="Feedback of interviews..."/>

Form
<form>

<field name="line_ids" nolabel="1"
context="{'default_lot_id':id,'default_line_type':'in_out',
'tree_view_ref'​:'jewelry.​view_production_lot_line_tree_in_out'​}"/>
...
</form>

<record id="​view_production_lot_line_tree_in_out​" model="ir.ui.view">


<field name="name">Production Lot Line</field>
<field name="model">production.lot.line</field>
<field name="arch" type="xml">
<tree decoration-muted="line_type == 'input'"
decoration-danger="(line_type == 'in_out') and (state in ['progress','done']) and (output_qty
&lt; 1)"
decoration-warning="(line_type == 'in_out') and (state in ['progress','done']) and (output_qty
&lt; input_qty)">
<field name="line_type" invisible="1" />
<field name="workorder_id" />
<field name="product_id" />
<field name="state" />
<field name="input_qty" />
<field name="output_qty" />
<field name="assigned_pt" widget="progressbar" />
<button type="object" name="action_waiting" string="Pending" icon="fa-stop-circle"
attrs="{'invisible':['|',('line_type','=','input'),('state','not in',['draft'])]}" />
<button type="object" name="action_draft" string="Draft" icon="fa-play-circle"
states="waiting" />
</tree>
</field>
</record>

Tree

Delete
Disable delete button on x2many tree list
<tree string="My Tree" ​delete="0"​>
….
</tree>

Create
Hide Add an item on x2many tree list
<field name="stockmove_ids" nolabel="1">
<tree ​create="0"​>
<field name="id" invisible="1" />
<field name="state" invisible="1" />
<field name="product_id" />
<field name="product_uom_qty" />
<field name="product_uom" />
<field name="location_id" />
<field name="location_dest_id" />
<button name="%(jewelry.wol_stockmove)d"
string="Edit" type="action"
context="{'default_id':id}"
groups="base.group_user" icon="fa-pencil" />
</tree>
</field>

Editable
<tree ​editable="bottom"​ create="false" delete="false">
...
</tree>

Dynamic readonly

<field name="item_work_ids" colspan="2" nolabel="1"


attrs="{'​readonly​':[('state','not in',['ready','progress'])]}"
context="{'tree_view_ref':'jewelry.view_item_work_lot_stone2_editable_tree}" />

<record id="view_item_work_lot_stone2_editable_tree" model="ir.ui.view">


<field name="name">Item Work</field>
<field name="model">item.work</field>
<field name="arch" type="xml">
<tree create="0" delete="0" editable="bottom" decoration-warning="item_scrap_id != False">
<field name="product_id"/>
<field name="color_id"/>
<field name="size_id"/>
<field name="state"/>
<field name="stone1_weight" sum="Stone1 weight"/>
<field name="stone2_weight" sum="Stone2 weight"/>
<field name="stone_weight" sum="Stone weight"/>
<field name="item_scrap_id" invisible="1"/>
</tree>
</field>
</record>

One2many select only

<field name="machine_ids" domain="[('workcenter_id','=',workcenter_id)]" ​widget="many2many"​>


<tree>
<field name="name" />
<field name="circuit_id" />
<field name="circuit_pin" />
<field name="qty_producing" />
<field name="qty_produced" />
</tree>
</field>
Colors
<tree ​colors="gray:state=='done';red:state=='cut'; red:loss_percentage>1.5"​>
<field name=”state” />
<field name=”loss_percentage” />

</tree>
<tree decoration-muted="state in ['done','cancel']" decoration-warning="state == 'progress'">
<field name="name" />
<field name="state" />
...
</tree>
<tree decoration-muted="line_type == 'input'">
<field name="line_type" invisible="1" />
...
</tree>

<tree decoration-muted="line_type == 'input'"


decoration-danger="(line_type == 'in_out') and (state in ['progress','done']) and (output_qty &lt; 1)"
decoration-warning="(line_type == 'in_out') and (state in ['progress','done']) and (output_qty &lt; input_qty)">

</tree>

Example: Manufacturing order tree view

<tree string="Manufacturing Orders" default_order="date_planned_start desc"


decoration-bf="message_needaction==True"
decoration-info="state=='confirmed'"
decoration-danger="date_planned_start&lt;current_date and state not in ('done','cancel')"
decoration-muted="state in ('done','cancel')">
<field name="message_needaction" invisible="1"/>
<field name="name"/>
<field name="date_planned_start"/>
<field name="product_id"/>
<field name="product_qty" sum="Total Qty" string="Quantity"/>
<field name="product_uom_id" string="Unit of Measure"
options="{'no_open':True,'no_create':True}" groups="product.group_uom"/>
<field name="availability"/>
<field name="routing_id" groups="mrp.group_mrp_routings"/>
<field name="origin"/>
<field name="state"/>
</tree>
Search

<record id="action_casting_tree_mgt" model="ir.actions.act_window">


<field name="name">Casting Tree</field>
<field name="res_model">casting.tree</field>
<field name="view_type">form</field>
<field name="view_mode">tree,form</field>
<field name="context" eval="{​'search_default_todos'​:1}"/>
</record>

<record id="view_casting_tree_search" model="ir.ui.view">


<field name="name">Casting Tree Search</field>
<field name="model">casting.tree</field>
<field name="arch" type="xml">
<search>
<field name="name" />
<field name="metal_type_id" />
<field name="casting_batch_id" />
<filter name="​todos​" string="ToDos"
domain="[('state','not in',['done','cancel'])]"
context="{'group_by':'metal_type_id'}"/>
<group string="Group By">
<filter name="metal_type" string="Metal"
context="{'group_by':'metal_type_id'}" />
</group>
</search>
</field>
</record>

Inherit

Xpath
Example:

<xpath expr="//page/field[@name='order_line']/tree/field[@name='price_total']" position="after">


<button type="object" name="action_open_form" icon="fa-list" string="Open"/>
</xpath>

Get the first group element right after form element


<xpath expr="//form/group[1]" position="before">
<header>
<button name="generate_allocation_leaves" type="object" string="Generate allocation leaves"
class="oe_highlight" attrs="{'invisible':[('allocation_scheme','!=','scheme')]}"/>
</header>
</xpath>

Attributes

Groups

<button name="archive_applicant" position="​attributes​">


​<attribute name="groups">feos_survey.group_top_manager,feos_survey.group_middle_manager</attribute>
</button>

<openerp>
<data>
<record model="ir.ui.view" id="view_crm_lead_form_inherited">
<field name="model">crm.lead</field>
<field name="inherit_id" ref="crm.view_crm_lead_form" />
​<field name="groups_id" eval="[(6, 0, [ref('base.group_sale_salesman')])]"/>
<field name="arch" type="xml">

<field name="phone" position="attributes">


<attribute name="readonly">True</attribute>
</field>

</field>
</record>

</data>
</openerp>

Invisible
<button name="create_employee_from_applicant" position="attributes">
<attribute name="​attrs​">​{'invisible':[('permitted','=',False)]}​</attribute>
</button>

Widgets

http://ludwiktrammer.github.io/odoo/form-widgets-many2many-fields-options-odoo.html

Field type Widget Description

many2many many2many
many2many_tags
many2many_checkboxes
many2many_kanban
x2many_counter
many2many_binary

binary widget=”image” Add class ‘​oe_avatar​’ for better UI

State (selection) widget=”statusbar”

Sequence widget="handle"

X2many widgets

many2many
many2many_tags

many2many_checkboxes

many2many_kanban

x2many_counter
many2many_binary

Action with selected views

<record id="person_lst_act" model="ir.actions.act_window">


<field name="name">Person</field>
<field name="res_model">res.partner</field>
<field name="view_type">form</field>
<field name="view_mode">tree,form</field>
<field name="view_ids"
eval="[(5, 0, 0),
(0, 0, {'view_mode': 'tree', 'view_id' : ref('person_custom_tree_view')}),
(0, 0, {'view_mode': 'form', 'view_id': ref('person_custom_form_view')}),
(0, 0, {'view_mode': 'kanban', 'view_id' : ref('person_custom_kanban_view')})]"/>
</record>

Function
Khi submit 1 Website form (module : website_form), hàm extract_data (file:
website_form/controllers/main.py) của Website form sẽ quyết định xem dữ liệu đó có được phép ghi vào
field tương ứng của Model hay không dựa trên whitelist (_get_form_writable_fields()).

Nên nếu muốn 1 field mới, ví dụ birthday, được phép lưu vào model khi form được submit thì phải khai
báo field này nằm trong whitelist của Model đó.

Module: website_form
File: website_form/models/models.py

class website_form_model_fields(models.Model):
​ """ fields configuration for form builder """
_name = 'ir.model.fields'
_inherit = 'ir.model.fields'
….

@api.model
def ​formbuilder_whitelist​(self, model, fields):
"""
:param str model: name of the model on which to whitelist fields
:param list(str) fields: list of fields to whitelist on the model
:return: nothing of import
"""
# postgres does *not* like ``in [EMPTY TUPLE]`` queries
if not fields: return False

# only allow users who can change the website structure


if not self.env['res.users'].has_group('website.group_website_designer'):
return False

# the ORM only allows writing on custom fields and will trigger a
# registry reload once that's happened. We want to be able to
# whitelist non-custom fields and the registry reload absolutely
# isn't desirable, so go with a method and raw SQL
self.env.cr.execute(
"UPDATE ir_model_fields"
" SET website_form_blacklisted=false"
" WHERE model=%s AND name in %s", (model, tuple(fields)))
return True

website_form_blacklisted = fields.Boolean(
'Blacklisted in web forms', default=True, index=True, # required=True,
help='Blacklist this field for web forms'
)

Module: website_hr_recruitment
File: data/config_data.xml

<function model="ir.model.fields" name="formbuilder_whitelist">


<value>hr.applicant</value>
<value eval="[
'description',
'email_from',
'partner_name',
'partner_phone',
'job_id',
'department_id',
]"/>
</function>

Nếu muốn thêm field names vào danh sách whitelist:

Module: mymodule
File: data/config_data.xml

<function model="ir.model.fields" name="formbuilder_whitelist">


<value>hr.applicant</value>
<value eval="['birthday','partner_mobile','salary_expected','availability']"/>
</function>

Lưu ý: hàm python là formbuilder_whitelist(self, model, fields). Value đầu tiên tương ứng với hr.applicant,
value thứ 2 tương ứng với danh sách các field thêm vào.
Security

Ref: ​https://www.odoo.yenthevg.com/creating-security-groups-odoo/

User groups

Reference hr_recruitment_security.xml

<record id="hr_applicant_comp_rule" model="ir.rule">


<field name="name">Applicant multi company rule</field>
<field name="model_id" ref="model_hr_applicant"/>
<field eval="True" name="global"/>
<field name="domain_force">['|',('company_id','=',False),('company_id','child_of',[user.company_id.id])]</field>
</record>

<record id="group_hr_recruitment_user" model="res.groups">


<field name="name">Officer</field>
<field name="category_id" ref="base.module_category_hr_recruitment"/>
<field name="implied_ids" eval="[(4, ref('hr.group_hr_user'))]"/>
</record>

<record id="group_hr_recruitment_manager" model="res.groups">


<field name="name">Manager</field>
<field name="category_id" ref="base.module_category_hr_recruitment"/>
<field name="implied_ids" eval="[(4, ref('group_hr_recruitment_user'))]"/>
<field name="users" eval="[(4, ref('base.user_root'))]"/>
</record>

<record id="base.default_user" model="res.users">


<field name="groups_id" eval="[(4,ref('hr_recruitment.group_hr_recruitment_manager'))]"/>
</record>

Define a new res.groups


Security.xml
<odoo>
<data>
<record id="module_categ_feos_hr_survey" model="ir.module.category">
<field name="name">FEOS HR Survey</field>
</record>

<record id="group_top_manager" model="res.groups">


<field name="name">Top Manager</field>
<field name="category_id" ref="feos_survey.module_categ_feos_hr_survey"/>
<field name="implied_ids" eval="[(4, ref('hr_recruitment.group_hr_recruitment_manager'))]"/>
<field name="users" eval="[(4, ref('base.user_root'))]"/>
</record>

<record id="group_middle_manager" model="res.groups">


<field name="name">Middle Manager</field>
<field name="category_id" ref="feos_survey.module_categ_feos_hr_survey"/>
<field name="implied_ids" eval="[(4, ref('hr_recruitment.group_hr_recruitment_user'))]"/>
</record>
</data>
</odoo>

Views inherit

<record model="ir.ui.view" id="feos_survey_crm_case_form_view_job">


<field name="model">hr.applicant</field>
<field name="inherit_id" ref="hr_recruitment.crm_case_form_view_job" />
<field name="arch" type="xml">
<button name="archive_applicant" position="attributes">
<attribute name="groups">
Feos_survey.group_top_manager,feos_survey.group_middle_manager
</attribute>
</button>
</field>
</record>

Example 2:

<odoo>
<data noupdate="0">

<record id="​group_hr_recruitment_interviewer​" model="res.groups">


<field name="name">Interviewer</field>
<field name="category_id" ref="base.module_category_hr_recruitment"/>
</record>

<record id="hr_recruitment.group_hr_recruitment_user" model="res.groups">


<field name="implied_ids" eval="[(4,
ref('feos_hr_interview.group_hr_recruitment_interviewer'))]"/>
</record>

<record id="interview_answer_access_rule" model="ir.rule">


<field name="name">Intervier answer access rule</field>
<field name="model_id" ref="model_hr_stage_interview_answer"/>
<field name="groups" eval="[(4,ref('feos_hr_interview.group_hr_recruitment_interviewer'))]"
/>
<field name="perm_read" eval="0"/>
<field name="perm_write" eval="1"/>
<field name="perm_create" eval="0"/>
<field name="perm_unlink" eval="0"/>
<field name="domain_force">[('create_uid','=',user.id)]</field>
</record>

<record id="job_interviewer_access_rule" model="ir.rule">


<field name="name">Intervier job access rule</field>
<field name="model_id" ref="model_hr_job"/>
<field name="groups" eval="[(4,ref('feos_hr_interview.group_hr_recruitment_interviewer'))]"
/>
<field name="perm_read" eval="1"/>
<field name="perm_write" eval="0"/>
<field name="perm_create" eval="0"/>
<field name="perm_unlink" eval="0"/>
<field name="domain_force">[('access_user_ids','in',[user.id])]</field>
</record>

<record id="applicant_interviewer_access_rule" model="ir.rule">


<field name="name">Intervier applicant access rule</field>
<field name="model_id" ref="model_hr_applicant"/>
<field name="groups" eval="[(4,ref('feos_hr_interview.group_hr_recruitment_interviewer'))]"
/>
<field name="perm_read" eval="1"/>
<field name="perm_write" eval="0"/>
<field name="perm_create" eval="0"/>
<field name="perm_unlink" eval="0"/>
<field name="domain_force">[('job_id.access_user_ids','in',[user.id])]</field>
</record>

</data>
</odoo>

Field access
We can restrict access using Python code or View xml
Python
class HrJob(models.Model):
_inherit = 'hr.job'

interviewer_ids = fields.Many2many('res.users',string='Interviewers',
groups="hr_recruitment.group_hr_recruitment_user",
relation="hr_job_user_interviewer",column1="job_id", column2="user_id")

or
View.xml
<field name="interviewer_ids" widget="many2many_tags" options="{'no_create':True}"
groups=”hr_recruitment.group_hr_recruitment_user” />

Views inherit for Groups


<record model="ir.ui.view" id="feos_hr_perm_interviewer_crm_case_form_view_job">
<field name="model">hr.applicant</field>
<field name="inherit_id" ref="hr_recruitment.crm_case_form_view_job" />
​<field name="groups_id" eval="[(6,0,[ref('group_hr_recruitment_interviewer')])]"/>
<field name="arch" type="xml">
<button name="create_employee_from_applicant" position="attributes">
<attribute name="attrs">{'invisible':True}</attribute>
</button>
<button name="archive_applicant" position="attributes">
<attribute name="attrs">{'invisible':True}</attribute>
</button>
</field>
</record>

Other Features
Environment
environment​ ,stored in self.env , contains the following:
● self.env.cr : This is a database cursor
● self.env.user : This is the user executing the action
● self.env.context : This is context, which is a Python dictionary containing various information such
as the language of the user, his configured time zone, and other specific keys that can be set at
run time by the actions of the user interface
● self.env[‘res.partner’]

Context

Example: The workaround is to put a marker in the context to be checked to break the ​recursion

class MyModel(models.Model):
@api.multi
def write(self, values):
super(MyModel, self).write(values)
if self.env.​context.get​('MyModelLoopBreaker'):
return
self = self.​with_context​(MyModelLoopBreaker=True)
self.compute_things() # can cause calls to writes

Example 2:
class ProductProduct(models.Model):
_inherit = 'product.product'

@api.model
def stock_in_location(self,location):
product_in_loc = self.with_context(location=location.id,active_test=False)
all_products = product_in_loc.search([])
stock_levels = []
for product in all_products:
if product.qty_available:
stock_levels.append( (product.name, product.qty_available) )
return stock_levels

self.with_context() with some keyword arguments. This returns a new version of self (which is a
product.product recordset) with the keys added

It is also possible to pass a dictionary to self.with_context() , in which case the dictionary is used as the
new context, overwriting the current one.

new_context = self.env.context.copy()
new_context.update({
'location': location.id,
'active_test': False})
product_in_loc = self.with_context(new_context)

Example: set weight for Option color and size wizard:

wizard.py
...
@api.one
def set_weight(self):
print '------------ set_weight',self
domain = [('wo_stone_id','=',self._context.get('workorder_id')),('color_id','=',self.color_id.id)]
if self.size_id:
domain.append(('size_id','=',self.size_id.id))
new_context = self.env.context.copy()
new_context.update({'workcenter_id':self._context.get('workcenter_id')})
print '\n --> context',self.env.context
options = self.with_context(new_context).env['production.option'].search(domain)
for opt in options:
opt.avg_stone_weight = self.weight
stone_bundles = [bi for bi in opt.bundle_ids if bi.is_stone_part == True]
stone_part_count = sum(sb.bom_ratio for sb in stone_bundles)
print 'stone_bundles',stone_part_count,stone_bundles
for bi in stone_bundles:
bi.part_option_id.avg_stone_weight = self.weight/stone_part_count
options.set_full_quantity()
# options.with_context(workcenter=self._context.get('workcenter_id')).set_full_quantity()
return options

production_option.py

@api.multi
def set_full_quantity(self):
print '----------------> set_full_quantity',self._context
if 'workcenter_id' in self._context:
# part_option_ids = []
if self._context['workcenter_id'] == self.env.ref('jewelry.wc_wax_injection').id:
self.wax_qty = self.product_qty
for bi in self.bundle_ids:
bi.part_option_id.wax_qty = bi.bom_ratio*self.product_qty
# bi.part_option_id.wax_qty += bi.bom_ratio*self.product_qty
elif self._context['workcenter_id'] == self.env.ref('jewelry.wc_stone_preparation').id:
self.stone_qty = self.product_qty
for bi in self.bundle_ids:
bi.part_option_id.stone_qty = bi.bom_ratio*self.product_qty

elif self._context['workcenter_id'] == self.env.ref('jewelry.wc_plant_tree').id:


self.planted_qty = self.product_qty
for bi in self.bundle_ids:
bi.part_option_id.planted_qty = bi.bom_ratio*self.product_qty

# Process for nested options


if self._context['workcenter_id'] in [self.env.ref('jewelry.wc_wax_injection').id,
self.env.ref('jewelry.wc_stone_preparation').id, self.env.ref('jewelry.wc_plant_tree').id ]:
for bi in self.bundle_ids:
if (bi.part_option_id.bundle_ids) and (len(bi.part_option_id.bundle_ids) > 0):
for opt_bi in bi.part_option_id.bundle_ids:
opt_bi.part_option_id.set_full_quantity()
return True

workorder_inherit.xml
<field name="option_plant_ids" context="{'default_workorder_id':id}" options="{'no_create':True}">
<tree>
<field name="color_id"/>
<field name="size_id"/>
<field name="product_qty" sum="Total quantity"/>
<field name="planted_qty" sum="Total"/>
<field name="planted_remain_qty" />
<button name="set_full_quantity" type="object" icon="fa-check"
context="{'workcenter_id':%(jewelry.wc_plant_tree)d}"
attrs="{'invisible':[('planted_qty','!=',0)]}"/>
</tree>
</field>

Import Data

data/default_data.xml
<?xml version="1.0" encoding="UTF-8"?>
<openerp>
<data>
<record id="​category_jewelry​" model="product.category">
<field name="name">Jewelry</field>
<field name="property_cost_method">standard</field>
<field name="property_valuation">manual_periodic</field>
</record>

<record id="​category_parts​" model="product.category">


<field name="name">Parts</field>
<field name="property_cost_method">standard</field>
<field name="property_valuation">manual_periodic</field>
</record>

<record id="​category_casting​" model="product.category">


<field name="name">Casting</field>
<field name="property_cost_method">standard</field>
<field name="property_valuation">manual_periodic</field>
</record>
</data>
</openerp>

To reference in module:
self.env.ref(‘jewelry.category_parts’).id ⇒ 8

@api.onchange('line_type')
@api.depends('mo_id')
def _onchange_linetype(self):
pids = []
# Collect all output workorder line pids
for r in self.mo_id.workcenter_lines:
for l in r.workorder_line_output_ids:
if self.id != l.id:
# Only add if this not current line
pids.append(l.product_id.id)
if self.line_type in ['input','in_out']:
pids.extend(map(lambda m: m.product_id.id, self.mo_id.move_lines))
if len(pids) > 0:
result = {'domain':{'product_id':[('id','in',pids)]}}
if self.product_id.id not in pids:
self.product_id = False
else:
print '--- Created ids',self.mo_id.move_created_ids
pids.extend(map(lambda p: p.product_id.id, self.mo_id.move_created_ids))
result = {'domain':{
'product_id':['|',
('id','in',pids),
('categ_id.id','=',self.env.ref('jewelry.category_parts').id)​]}}
return result

class ​HREmployee​(models.Model):
_inherit = 'hr.employee'

calendar_id = fields.Many2one('resource.calendar',
default=lambda self: self.env.ref('jewelry_base.calendar_default')​)

Field Domain:

class CastingMetalType(models.Model):
_name = 'casting.metal.type'
_description = 'Metal Type'
name = fields.Char(string='Name',required=True,copy=False)

def ​_get_product_attr_domain​(self):
return [('attribute_id','=', ​self.env.ref('jewelry.product_attr_metal_type').id ​)]

product_attribute_id = fields.Many2one(comodel_name='product.attribute.value',string='Product
Attribute',domain=​_get_product_attr_domain​)

View.xml

<record id="jewelry_product_template_only_form_view_inherit" model="ir.ui.view">


<field name="model">product.template</field>
<field name="inherit_id" ref="product.product_template_only_form_view" />
<field name="arch" type="xml">
<field name="uom_po_id" position="after">
<field name="weight_on_qty" attrs="{'invisible':[('categ_id','!=', ​%(jewelry.category_parts)d​ )]}" />
</field>
</field>
</record>
….
<record id="view_casting_tree_line_form" model="ir.ui.view">
<field name="name">Casting Tree Line</field>
<field name="model">casting.tree.line</field>
<field name="arch" type="xml">
<form>
<sheet>
<group name="group_main">
<field name="product_id" ​domain="[('categ_id','in',[%(jewelry.category_casting)d])]" ​/>
<field name="product_qty" />
</group>
</sheet>
</form>
</field>
</record>

<record id="action_casting_workorder_mgt" model="ir.actions.act_window">


<field name="name">Casting Workorders</field>
<field name="res_model">mrp.workorder</field>
<field name="view_type">form</field>
<field name="view_mode">tree,form</field>
<field name="domain" eval="​[('workcenter_id','in',[ ref('jewelry.wc_casting') ] )]​" />
</record>
Button action from import data

<record id="​action_account_invoice_refund​" model="ir.actions.act_window">


<field name="name">Refund Invoice</field>
<field name="res_model">account.invoice.refund</field>
<field name="view_type">form</field>
<field name="view_mode">tree,form</field>
<field name="view_id" ref="view_account_invoice_refund"/>
<field name="target">new</field>
</record>

<button name="​%(action_account_invoice_refund)d​"
type='​action​'
string='Ask Refund'
states='open,paid'
groups="account.group_account_invoice"/>

Domain

Today condition
XML:

<record id="view_production_lot_scrap_search" model="ir.ui.view">


<field name="name">Production Lot Scrap</field>
<field name="model">production.lot.scrap</field>
<field name="arch" type="xml">
<search>
<filter name="today" string="Today" context="{'group_by':'product_id'}"
domain="[
('time', '&gt;=', datetime.datetime.now().replace(hour=0, minute=0, second=0)),
('time', '&lt;=', datetime.datetime.now().replace(hour=23, minute=59, second=59)),
('state','=','draft')
]"/>
</search>
</field>
</record>

Python:
domain = [ '|',
('state','not in',['done','cancel']),'&',
('date_order', '>=', datetime.datetime.now().strftime('%Y-%m-%d 00:00:00')),
('date_order', '<=', datetime.datetime.now().strftime('%Y-%m-%d 23:23:59'))]
so_ids = self.env['sale.order'].sudo().search_read(domain,['name'])
Server

Page 144

Flow
Sale.order -> MO
Mindmap
Simple

sale.order 1

sale.order.line 3
(sale.order.line, 3)_action_procurement_create() --> new (procurement.order,5)
# TRACING: (procurement.order,5).sale_line_id.id = 3
(procurement.order,5).run()

procurement.order 5
(procurement.order,5).run()
if rule_id.action == 'move':
create 'stock.move' --> new (stock.move,7)
# TRACING: (stock.move,7).procurement_id.id = 5
(stock.move,7).action_confirm()

stock.move 7
(stock.move,7).action_confirm()
procurements.create(move._prepare_procurement_from_move()) --> new (procurement.order, 6)
# TRACING: (procurement.order, 6).move_dest_id.id = 7
(procurement.order, 6).run()
[?] if rule_id.action == 'manufacture':
self.make_mo() --> new (mrp.production,9)

-----------------------------------------------------------------------------------

(sale.order, 54)
(sale.order.line, 100)
(procurement.order, 226) rule:'WH:Stock -> CustomersMTO', action:'move'
(stock.move,442) rule:'WH:Stock -> CustomersMTO', action:'move'
(procurement.order, 227) rule:'My Company: Manufacture', action:'manufacture'
(mrp.production, 155)
(stock.move, 443) rule: False, action: False

Code inside

-----------------------------------------------------------------------------------
sale.order
448: def ​action_confirm​(self):
order.order_line._action_procurement_create()

-----------------------------------------------------------------------------------
sale.order.line
641: def ​_action_procurement_create​(self):
658: vals = line.order_id._prepare_procurement_group()
659: line.order_id.procurement_group_id = self.env["procurement.group"].create(vals)
661: vals = line._prepare_order_line_procurement(
group_id=line.order_id.procurement_group_id.id
)
662: vals['product_qty'] = line.product_uom_qty - qty
663: new_proc = self.env["procurement.order"].create(vals)
668: new_procs.run()

625: @api.multi
def ​_prepare_order_line_procurement​(self, group_id=False):
self.ensure_one()
return {
'name': self.name,
'origin': self.order_id.name,
'date_planned': datetime.strptime(
self.order_id.date_order,
DEFAULT_SERVER_DATETIME_FORMAT) +
timedelta(days=self.customer_lead),
'product_id': self.product_id.id,
'product_qty': self.product_uom_qty,
'product_uom': self.product_uom.id,
'company_id': self.order_id.company_id.id,
'group_id': group_id,
'sale_line_id': self.id }

-----------------------------------------------------------------------------------
stock/models/procurement.py
196: def ​run​(self, autocommit=False):
...
199: res = super(ProcurementOrder, new_self).run(autocommit=autocommit) # will trigger _run()
...
203: new_self.filtered(lambda order: order.state == 'running' and order.rule_id.action ==
'move').mapped('move_ids').filtered(lambda move: move.state == 'draft').action_confirm()

185: def ​_run​(self):


if self.rule_id.action == 'move':
if not self.rule_id.location_src_id:
self.message_post(body=_('No source location defined!'))
return False
# create the move as SUPERUSER because the current user may not have the rights to do
it (mto product launched by a sale for example)
191: self.env['stock.move'].sudo().create(self._get_stock_move_values())
return True
return super(ProcurementOrder, self)._run()

138: def ​_get_stock_move_values​(self):


...
155: return { ... }

-----------------------------------------------------------------------------------
stock/stock_move.py
450: @api.multi
451: def ​action_confirm​(self):
...
483: # create procurements for make to order moves
procurements = self.env['procurement.order']
for move in move_create_proc:
procurements |= procurements.create(move._prepare_procurement_from_move())
if procurements:
procurements.run()

-----------------------------------------------------------------------------------
mrp/models/procurement.py
31: def ​_run​(self):
self.ensure_one()
if self.rule_id.action == 'manufacture':
# make a manufacturing order for the procurement
return self.make_mo()[self.id]
return super(ProcurementOrder, self)._run()

76: def ​make_mo​(self):


85: production = ProductionSudo.create(
procurement._prepare_mo_vals(bom))
Feature development

Check if a module is installed

Example: check if Attendance (hr_attendance) module is installed

atd_module = self.env['ir.module.module'].search([('name','=','hr_attendance')])
atd_module_installed = False # Hr Attendance module installed
if atd_module and atd_module.state == 'installed':
atd_module_installed = True

Wizard

Views.xml

<act_window id="launch_the_wizard"
name="Launch the Wizard"
src_model="context.model.name"
res_model="wizard.model.name"
view_mode="form"
target="new"
multi=”1”
key2="client_action_multi"/>

Model.py

def _get_default_students(self):
return self.env['‘school.student’'].browse(self._context.get(​'active_ids'​))

student_ids = fields.Many2many(‘school.student’,string=’Students’,default=_get_default_students)

Button open Form


Example:

class SaleOrderLine(models.Model):
_inherit = 'sale.order.line'
...
@api.multi
def action_open_form(self):
print 'action_open_line_form',self
self.ensure_one()
​action = self.env.ref('jewelry.​action_view_sale_order_line​').read()[0]
action['res_id'] = self.id
return action

Views.xml

<record id="​view_sale_order_line_view_form​" model="ir.ui.view">


<field name="name">Sale Order Line</field>
<field name="model">sale.order.line</field>
<field name="arch" type="xml">
<form>
<sheet>
<group>
<group>
<field name="order_id"/>
<field name="product_id"/>
<field name="product_uom_qty"/>
</group>
<group>
<field name="name"/>
</group>
</group>
<group name="items" string="Items">
<field name="id" invisible="1"/>
<field name="item_ids" colspan="2" nolabel="1"
context="{'default_sale_line_id': id}"/>
</group>
</sheet>
</form>
</field>
</record>

<record id="​action_view_sale_order_line​" model="ir.actions.act_window">


<field name="name">Sale Order Line</field>
<field name="res_model">sale.order.line</field>
<field name="view_type">form</field>
<field name="view_mode">tree,form</field>
<field name="view_id" ref="​view_sale_order_line_view_form​"/>
<field name="target">current</field>
</record>

Smart buttons

Models.py

class mrpProductionInherit(models.Model):
_inherit = 'mrp.production'
item_work_ids = fields.One2many(comodel_name='item.work',inverse_name='mo_id', string='Item Works')
item_work_qty = fields.Float(string='Item Work qty',compute='_get_item_work_qty',help='Technical field
counting item.works ')

@api.depends('item_work_ids')
@api.multi
def _get_item_work_qty(self):
for r in self:
r.item_work_qty = len(r.item_work_ids)

@api.multi
def action_view_production_item_works(self):
​action_rec = self.env.ref('jewelry.action_production_item_works')
action = action_rec.read()[0]
ctx = dict(self.env.context)
​ctx.update({
'search_default_mo_id': self.ids[0],
'group_by':['workcenter_id','color_id'],
})
action['context'] = ctx
return action

Inherit_mrp.xml

<record id="view_mrp_production_form_inherit" model="ir.ui.view">


<field name="model">mrp.production</field>
<field name="inherit_id" ref="mrp.mrp_production_form_view" />
<field name="arch" type="xml">
<xpath expr="//sheet/div[@name='button_box']" position="inside">
​<button name="action_view_production_item_works"
type="object" class="oe_stat_button" icon="fa-diamond"
attrs="{'invisible':[('item_work_qty','=',0)]}">
<field string="Items Works" name="item_work_qty" widget="statinfo"/>
</button>
</xpath>
</field>
</record>

Views.xml

<record id="​production_item_work_tree​" model="ir.ui.view">


<field name="name">Item Works</field>
<field name="model">item.work</field>
<field name="arch" type="xml">
<tree>
<field name="id" groups="base.group_no_one"/>
<field name="partner_id"/>
<field name="mo_id"/>
<field name="workcenter_id"/>
<field name="product_id"/>
<field name="color_id"/>
<field name="size_id"/>
<field name="state"/>
<field name="stone1_weight"/>
<field name="stone2_weight"/>
<field name="stone_weight"/>
</tree>
</field>
</record>

<record id="​production_item_work_search​" model="ir.ui.view">


<field name="name">Item Works</field>
<field name="model">item.work</field>
<field name="arch" type="xml">
<search>
<field name="mo_id"/>
</search>
</field>
</record>

<record model="ir.actions.act_window" id="​action_production_item_works​">


<field name="name">Item Works</field>
<field name="res_model">item.work</field>
<field name="view_mode">tree,form</field>
​<field name="view_id" ref="jewelry.production_item_work_tree"></field>
<field name="search_view_id" ref="jewelry.production_item_work_search"/>
<field name="help" type="html">
<p>There is no item works in this produciton order.</p>
</field>
</record>

Danh sách hiển thị ra mặc định filter theo lệnh sản xuất đang xem (MO0002) và group lại theo Workcenter
và Màu.
Tree list button trigger Wizard
casting.py
# -*- coding: utf-8 -*-

from odoo import models, fields, api, exceptions


from odoo.addons import decimal_precision as dp

class ​CastingTreeLine​(models.Model):
_name = 'casting.tree.line'

@api.multi
def ​add_scrap​(self):
print 'add_scrap',self
ctx = dict(self.env.context)
print 'context',ctx
self.ensure_one()
action = self.env.ref('jewelry.​act_casting_tree_scrap_move​').read()[0]
action['context'] = ctx
return action

Casting_scrap.xml

<?xml version="1.0" encoding="utf-8"?>


<odoo>

<record id="​view_casting_tree_scrap_wizard​" model="ir.ui.view">


<field name="name">Casting Tree Scrap Wizard</field>
<field name="model">casting.tree.scrap.wizard</field>
<field name="arch" type="xml">
<form string="Produce">
<group>
<field name="tree_line_id"/>
<field name="qty"/>
<field name="weight"/>
</group>
<footer>
<button name="action_save" type="object" string="Save" class="btn-primary"/>
<button string="Cancel" class="btn-default" special="cancel" />
</footer>
</form>
</field>
</record>

<record id="​act_casting_tree_scrap_move​" model="ir.actions.act_window">


<field name="name">Casting Scrap Move</field>
<field name="type">ir.actions.act_window</field>
<field name="res_model">casting.tree.scrap.wizard</field>
<field name="view_type">form</field>
<field name="view_mode">form</field>
<field name="context">{}</field>
<field name="target">new</field>
</record>

</odoo>

Casting_scrap.py

# -*- coding: utf-8 -*-

from odoo import models, fields, api, exceptions


from odoo.addons import decimal_precision as dp

# Ref:
# /mrp/wizard/mrp_product_produce.py
# /mrp/wizard/mrp_product_produce_views.xml

class ​CastingTreeScrap​(models.Model):
_name = 'casting.tree.scrap'
_description = 'Casting Tree Scrap'

tree_line_id = fields.Many2one(comodel_name='casting.tree.line',string='Casting Tree


Line',readonly=True,ondelete='cascade')
casting_tree_id = fields.Many2one(comodel_name='casting.tree',string='Casting
Tree',related='tree_line_id.casting_tree_id',store=True)
product_id =
fields.Many2one('product.product',string='Product',related='tree_line_id.product_id',readonly=True)
qty = fields.Float(string='Scrap quantity',digits=(6,0),readonly=True)
weight = fields.Float(string='Scrap weight',digits=dp.get_precision('Product Unit of
Measure'),readonly=True)

class ​CastingTreeScrapWizard​(models.TransientModel):
_name = 'casting.tree.scrap.wizard'

tree_line_id = fields.Many2one(comodel_name='casting.tree.line',string='Casting Tree


Line',readonly=True)
qty = fields.Float(string='Scrap quantity',digits=(6,0))
weight = fields.Float(string='Scrap weight',digits=dp.get_precision('Product Unit of Measure'))

@api.multi
def ​action_save​(self):
print '------------ action_save',self
for r in self:
vals = {
'tree_line_id':r.tree_line_id.id,
'qty':r.qty,
'weight':r.weight
}
print "CREATE casting tree scrap vals",vals
scrap = self.env['casting.tree.scrap'].create(vals)
return True

Config setting

Add tolerance_minutes integer value to Setting > General Settings


Config.py

# -*- coding: utf-8 -*-

from odoo import models, fields, api

class BaseConfigSettings(models.TransientModel):
_inherit = 'base.config.settings' # mandatory

tolerance_minutes = fields.Integer(string='Acceptance tolerance minutes',


help='If attendance time different with working time is less than this value, standard working
time is used to set Attendance time')

@api.model
def get_default_tolerance_minutes(self, fields):
return {
'tolerance_minutes': int( self.env['ir.config_parameter'].get_param(
'feos_hr.tolerance_minutes', 5) )
}

@api.multi
def set_tolerance_minutes(self):
self.ensure_one()
self.env['ir.config_parameter'].set_param('feos_hr.tolerance_minutes',
int(self.tolerance_minutes))

Config.xml
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data>
<record id="view_base_config_settings_form_inherit" model="ir.ui.view">
<field name="model">base.config.settings</field>
<field name="inherit_id" ref="base_setup.view_general_configuration" />
<field name="arch" type="xml">
<form position="inside">
<group name="feos_hr" string="FEOSCO HR">
<field name="tolerance_minutes"/>
</group>
</form>
</field>
</record>
</data>
</odoo>

TODO: Check sale.order code below:

_name = ‘sale.order’

@api.multi
def action_confirm(self):
for order in self:
order.state = 'sale'
order.confirmation_date = fields.Datetime.now()
if self.env.context.get('send_email'):
self.force_quotation_send()
order.order_line._action_procurement_create()
if ​self.env['ir.values'].get_default('sale.config.settings', 'auto_done_setting')​:
self.action_done()
return True

Resource.Calendar

Get_working_hours
Tính toán thời gian làm việc nằm trong khoảng quy định dựa trên calendar, ví dụ tính thời gian làm việc
trong giờ.

class HrAttendance(models.Model):
_inherit = "hr.attendance"

@api.multi
@api.depends('employee_id','worked_hours')
def _get_working_hours(self):
calendar = self.env.ref('feos_hr.calendar_standard')
for r in self:
if r.employee_id and r.check_out:
# calendar = r.employee_id.calendar_id
check_in = datetime.strptime(r.check_in,'%Y-%m-%d %H:%M:%S')
check_out = datetime.strptime(r.check_out,'%Y-%m-%d %H:%M:%S')
r.worked1_hours = calendar.​get_working_hours​(check_in,check_out)
r.worked2_hours = r.worked_hours - r.worked1_hours
return True

Get_working_intervals_of_day
Lấy thời gian làm việc theo ngày quy định trong Calendar

intervals = employee.calendar_id.​get_working_intervals_of_day​()
# intervals = [
(datetime.datetime(2017, 2, 6, 0, 30, 0, 642780), datetime.datetime(2017, 2, 6, 4, 30, 0, 642780)),
(datetime.datetime(2017, 2, 6, 6, 0, 0, 642780), datetime.datetime(2017, 2, 6, 10, 0, 0, 642780))
]
Sequence
Tạo sequence cho Model Name, ví dụ: lệnh sản xuất MO001 --> MO002 …

__manifest__.py


'data': [
...
'data/sequence.xml',
...
]

data/sequence.xml

<?xml version="1.0" encoding="utf-8"?>


<openerp>
<data>
<record id="sequence_casting_tree" model="ir.sequence">
<field name="name">Casting Tree Sequence</field>
<field name="code">casting.tree</field>
<field name="prefix">CT</field>
<field name="padding">4</field>
<field eval="1" name="number_increment"/>
</record>

</data>
</openerp>

model.py

class CastingTree(models.Model):
_name = 'casting.tree'
_description = 'Casting Tree'
_order = "name desc"

name = fields.Char(string='Name',readonly=True, copy=False)


@api.model
def ​create​(self,vals):
vals['name'] = ​self.env['ir.sequence'].next_by_code('casting.tree')
return super(CastingTree,self).create(vals)

CRON
CRON cho phép hệ thống thực thi một hàm định kỳ, vd: 1 ngày, 1 giờ hay 1 phút
Ví dụ : Tăng giá trị scores của res.partner 1 lên 1 mỗi phút

Lưu ý: số lượng config max_cron_threads trong config file.

max_cron_threads = 1

Models.py

class ResPartner(models.Model):
_inherit = 'res.partner'

scores = fields.Float(string='Scores',default=0)

class myCronTask(models.TransientModel):
_name = 'mycron.task'
@api.model
def increase_number(self):
c0 = self.env['res.partner'].search([('id','=',1)])
c0.scores += 1

Lưu ý: có thể dùng models.TransientModel hoặc models.Model để khai báo hàm chạy CRON.

Cron.xml
Ref: ​http://odoo-development.readthedocs.io/en/latest/odoo/models/ir.cron.html

<odoo>
<data>

<record id="cron_increase_number" model="ir.cron">


<field name="name">Cron Increase Number</field>
<field name="active" eval="True"/>
<field name="user_id" ref="base.user_root"/>
<field name="​interval_number​">​1​</field>
<field name="​interval_type​">​days​</field>
<field name="numbercall">-1</field>
<field name="​doall​">0</field>
<field name="​nextcall​" >2017-08-04 01:59:59</field>
<field name="model" eval="​'m
​ ycron.task​'​"/>
<field name="function" eval="​'​increase_number​'​" />
<field name="args" eval="" />
</record>

</data>
</odoo>

Lưu ý:

● nextcall​: Nếu không set nextcall, hệ thống sẽ tự động lấy một ngày nào đó, ví dụ: ngày của tháng
sau, khi đó CRON sẽ không chạy trong vòng 1 tháng !!!
● doall = 1​: Nếu hệ thống bị treo trong 1 ngày, thì khi hệ thống chạy lại, các CRON chạy trong 1
ngày đó (khi hệ thống bị treo) sẽ được kích hoạt chạy lại.
Nếu không muốn thì doall = 0​.
● interval_type​ : Interval Unit - It should be one value for the list: ​minutes, hours, days, weeks,
months​.

Mail template

__manifest__.py
...
'depends': ['mail'],
...

Template.xml
<odoo>
<data noupdate="1">

<record id="birthday_template" model="mail.template">


<field name="name">Birthday mail template</field>
<field name="email_from">${object.company_id and object.company_id.email or ''}</field>
<field name="subject">Congrats ${object.name}</field>
<field name="email_to">${object.email|safe}</field>
<field name="lang">${object.lang}</field>
<field name="model_id" ref="base.model_res_partner"/>
<field name="auto_delete" eval="False"/>
<field name="body_html">
<![CDATA[
<p>Dear ${(object.name)},<br/><br/>
Happy birthday to you.<br/>
Regards,<br/>
${(object.company_id.name)}
</p>
]]>
</field>
</record>

</data>
</odoo>

Lưu ý: có thể dùng Mail chimp để compose mail template, rồi export HTML , rồi copy, paste vào Template
.
Config outgoing mail

Lưu ý: đối với Gmail, cần phải bật chế độ "Allow less secure apps" để cho phép đăng nhập bằng app.
Models.py

# -*- coding: utf-8 -*-

from odoo import models, fields, api

class ResPartner(models.Model):
_inherit = 'res.partner'

@api.multi
def send_birthday_mail(self):
template = self.env.ref('mymail.birthday_template')
return ​self.env['mail.template'].browse(template.id).send_mail(self.id, force_send=True)
Views.xml

<odoo>
<data>

<record id="view_partner_form_inherit" model="ir.ui.view">


<field name="model">res.partner</field>
<field name="inherit_id" ref="base.view_partner_form" />
<field name="arch" type="xml">
<xpath expr="//sheet" position="before">
<header>
<button name="send_birthday_mail" string="Send Birthday" type="object"
class="oe_highlight"/>
</header>
</xpath>
</field>
</record>

</data>
</odoo>

Result
PYTHON ESC-POS (python 3)

Ubuntu Linux
Dùng lệnh lsusb để liệt kê danh sách các thiết bị đang kết nối usb vào máy --> Biết id của máy in vd:
0x04b8,0x0e11

# lsusb

Printer.py

#!/usr/bin/python3
# -*- coding: utf-8 -*-
from datetime import datetime
from time import sleep
import sys
from flask import Flask, request,jsonify
from flask_cors import CORS, cross_origin

app = Flask(__name__)
# cors = CORS(app) # this will allow CORS for all routes
cors = CORS(app, resources={r"/print/*": {"origins": "*"}})

from escpos.printer import Usb


myPrinter = False

def _init_printer():
global myPrinter
print('---------- Start to init_printer ---------')
printer_status = False
try_count = 1
while not printer_status:
try:
myPrinter = Usb(​0x04b8,0x0e11​)

# myPrinter.charcode('VIETNAM')
# myPrinter.charcode('MULTILINGUAL')
# myPrinter.charcode('TCVN-3-1')
# myPrinter.charcode('TCVN-3-2')
myPrinter.line_spacing(100)
printer_status = True
except:
print ('%s - Failed to init Printer' % str(try_count))
try_count += 1
printer_status = False
sleep(3)

# myPrinter = Usb(0x04b8,0x0e11)
# myPrinter.line_spacing(100)

_init_printer()

qty_fmt = '{:0,.0f}'
weight_fmt = '{:0,.2f}'
time_fmt = '{0:1.0f} giờ: {1:02.0f} phút'
# time_fmt = '{0:1.0f}h:{1:02.0f}'
# time_fmt = '{:0,.2f}'
day_fmt = '{:0,.2f}'
money_vn_fmt = '{:0,.0f}'
@app.route("/print/payroll",methods=['POST'])
def print_payroll():
data = request.get_json()
print ('print_payroll data',data)
# return jsonify(success=True)
try:
myPrinter.set(align="left",text_type="B",width=2,height=2)
myPrinter.text("%s\n\n" % data['employee'])

myPrinter.set(text_type="NORMAL")
# myPrinter.text("ID:\t%s\n" % data['name'])
# myPrinter.text("Lương:\t%s\n" % money_vn_fmt.format(data['salary']))
myPrinter.text("Lương:\t%s \t\tID: %s\n" % (money_vn_fmt.format(data['salary']) , data['name']) )
# myPrinter.text("Ngày:\t%s -- %s\n\n" % (data['start_date'], data['end_date']))
if data['payroll_type'] == 'manual':
# Manual payroll
myPrinter.text("Mục \t\tThời gian \tTiền \n")
myPrinter.text("------------------------------------------------\n")
if data['mworked1_days']:
myPrinter.text("Ngày làm \t%s ngày \t%s\n" %
(day_fmt.format(data['mworked1_days']), money_vn_fmt.format(data['mworked1_total'])))

if data['mworked1_hours']:
myPrinter.text("Trong giờ \t%s \t%s\n" %
(_format_working_time(data['mworked1_hours']), money_vn_fmt.format(data['mworked1_hours_total'])))
if data['mworked2_hours']:
myPrinter.text("Ngoài giờ \t%s \t%s\n" %
(_format_working_time(data['mworked2_hours']), money_vn_fmt.format(data['mworked2_hours_total'])))

else:
# Attendance payroll
myPrinter.text("Thời gian \t\tGiờ \tTiền \n")
myPrinter.text("------------------------------------------------\n")
if data['worked1_hours']:
myPrinter.text("Trong giờ \t%s \t%s\n" %
(_format_working_time(data['worked1_hours']), money_vn_fmt.format(data['worked1_total'])))
if data['worked2_hours']:
myPrinter.text("Ngoài giờ \t%s \t%s\n" %
(_format_working_time(data['worked2_hours']), money_vn_fmt.format(data['worked2_total'])))

myPrinter.text("------------------------------------------------\n")
myPrinter.set(align="right")
myPrinter.text("\nTổng\t %s\n" % money_vn_fmt.format(data['worked_total']))
if data['delivery_total']:
myPrinter.text("\tGiao hàng\t %s\n" % money_vn_fmt.format(data['delivery_total']))
if data['extra_total']:
myPrinter.text("\tThêm\t %s\n" % money_vn_fmt.format(data['extra_total']))
if data['debt_total']:
myPrinter.text("\tNợ\t %s\n" % money_vn_fmt.format(data['debt_total']))
myPrinter.text("___________________________\n")
# myPrinter.set(text_type="B",height=2,width=2)
myPrinter.set(height=2,width=2)
myPrinter.text("Thành tiền: %s" % money_vn_fmt.format(data['total']))
myPrinter.set(text_type="NORMAL")
if data['note']:
myPrinter.text("\n%s" % data['note'])

# Cut paper
myPrinter.cut()
except:
print ('------->Failed to print Employee Payroll',sys.exc_info()[0])
_init_printer()

return jsonify(success=True)

if __name__ == "__main__":
app.run(host='0.0.0.0', port=​5000​)

Vous aimerez peut-être aussi