Beruflich Dokumente
Kultur Dokumente
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
. /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
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
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;
}
}
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
2. Nginx config
server {
listen 80 default_server;
listen [::]:80 default_server ipv6only=on;
root /usr/share/nginx/html;
index index.html index.htm;
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:~$
Đầ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ề
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
....
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
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 |
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)
Example in Module:
# -*- coding: utf-8 -*-
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)
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)
@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.check_access_rights("read")
self.check_access_rule("read")
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’])
Time zone
UTC to User local time
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
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.
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 .
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 .
Example:
from odoo import models, fields, api
from datetime import timedelta as td
class LibraryBook(models.Model):
_name = 'library.book'
_description = 'Library Book'
@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)]
One2many
Many2one
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
string
size
translate
help
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.
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)]})
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
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'
@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'
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.
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
.
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
@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)
…
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}" />
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)
class Country(models.Model):
_name = 'res.country'
_description = 'Country'
_order = 'name'
_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'
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
@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>
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:
@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.
class HrApplicant(models.Model):
_inherit = 'hr.applicant'
@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)
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:
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 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.
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
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
Context
default values
Usage: default_field_name
<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>
Invisible
<field name="workcenter_id" options="{'no_create':True}"
attrs="{'invisible':[('move_type','in',['lot','state'])] }"/>
Required
Widgets
many2many_tags
one2many_list
selection
progressbar
selection
statusbar
handle
monetary
mail_thread
statinfo
contact
html
mail_followers
url
radio
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>
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
Inherit
Xpath
Example:
Attributes
Groups
<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>
</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
many2many many2many
many2many_tags
many2many_checkboxes
many2many_kanban
x2many_counter
many2many_binary
Sequence widget="handle"
X2many widgets
many2many
many2many_tags
many2many_checkboxes
many2many_kanban
x2many_counter
many2many_binary
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
# 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
Module: mymodule
File: data/config_data.xml
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
Views inherit
Example 2:
<odoo>
<data noupdate="0">
</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” />
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)
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
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>
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
<button name="%(action_account_invoice_refund)d"
type='action'
string='Ask Refund'
states='open,paid'
groups="account.group_account_invoice"/>
Domain
Today condition
XML:
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()
-----------------------------------------------------------------------------------
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()
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)
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
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
Views.xml
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 -*-
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
</odoo>
Casting_scrap.py
# 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'
class CastingTreeScrapWizard(models.TransientModel):
_name = 'casting.tree.scrap.wizard'
@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
class BaseConfigSettings(models.TransientModel):
_inherit = 'base.config.settings' # mandatory
@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>
_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
</data>
</openerp>
model.py
class CastingTree(models.Model):
_name = 'casting.tree'
_description = 'Casting Tree'
_order = "name desc"
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
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>
</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">
</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
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>
</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": "*"}})
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)