2020年2月27日 星期四

Odoo教育訓練學習筆記 (4)

  由於轉換職場跑道的緣故,許久沒有撰寫學習筆記或書摘心得等文章,原本自學內容以Python為核心,著重資料科學的技術與應用,但由於新工作的需求,必須轉而學習開放原始碼ERP系統-Odoo的開發,對於僅有類似系統分析經驗的我而言,這是非常難得的機會,可以讓我真正跨入系統工程師的領域。

  由於自己並非資訊相關科系畢業,基礎知識與技能相當貧乏,透過此次學習Odoo系統基礎建置與開發,我才初次接觸了如何在Windows系統透過VM來使用Linux系統,以及終端機指令的操作,來一步一步建置Odoo系統,包括所需的Python套件、PostgreSQL資料庫系統、pgAdmin資料庫管理工具、Odoo系統資料夾結構、Odoo系統設定文件、PyCharm整合開發環境、系統更新與維護方式等等,需要了解與學習的技術非常非常的多,自己也僅學習使用了一小部分的工具與技巧而已。

  系統開發由於牽涉到許多資料夾結構與不同文件如csvpyxml等,因此未來使用Git做為示範檔案存放空間的需求或許也將越來越迫切,現階段仍純粹在網誌整理、發布文章;本系列一共六篇學習筆記,為花費大量時間綜合整理Peter Wu老師課堂教學、《Odoo快速入門與實戰》簡體書,以及零散的網路教學文章而得,前五篇的學習筆記完全操作使用Odoo社群版,第六篇的學習筆記則加入「康虎雲報表」的安裝與教學,可至「康虎軟件工作室」官方網站下載相關軟體,配合學習筆記的說明來安裝使用。

  自學Odoo系統的建置開發相當不容易:在台灣,Odoo系統的知名度與普及性遠不如SAP、鼎新、Oracle等大廠,資訊廠商與開發人員的數量也相對稀少,就連參考書籍也完全沒有繁體中文的版本,僅有少數英文書與簡體書可以購買(而且這些書都寫得很糟),加上這幾年Odoo系統的快速發展,不同的版本之間有不小的變動,學會的技術可能過幾年就完全不能用了…。綜合上述許多原因,即便本系列學習筆記已經整理很清楚了,對於完全零經驗的開發者而言,相信仍舊難以閱讀、理解。

  目前Odoo系統在歐美與中國大陸的資源、社群與討論,都比台灣頻繁、密集得多,想要入門Odoo系統的學習者建議還是需要老師來帶領,才有辦法掌握操作、管理、開發的各項眉眉角角。

Odoo概論
l   Odoo遵循MVC(Models, Views, Actions)設計原則進行設計,將架構分為:
數據層-採用PostgreSQL
邏輯層-採用ORMORMOdoo的引擎,會根據我們的Python方法與對應的實體自動生成SQL語法。
視圖層-展示層。
l   base模組中的模型主要可分為兩種:
訊息資料庫(Information Repository)-其模組的外部ID一般以"ir."開頭-
ir.action.act_window
設置窗口的動作
ir.ui.menu
設置選單
ir.ui.view
用於處理視圖
ir.model
用於模型
ir.model.fields
用於處理模型的字段
資源(Resource)-其模組的外部ID一般以"res."開頭-
res.partner
諸如客戶、供應商或聯絡人等都是基於這個模型
res.company
設置公司相關的訊息
res.currency
處理與貨幣相關的功能
res.country
處理與國家相關的設置訊息
res.users
用於處理與用戶相關的功能
res.groups
與權限相關的功能都是基於這個模型

Odoo ORM API
l   在終端機使用Python 3 Shell操作,資料庫名稱以odoo12為例,若成功執行,畫面將顯示如下圖:
cd /opt/odoo/odoo

python3 odoo-bin shell -c /etc/odoo.conf -d odoo12

self


l   Odoo ORM是關係式,速度較慢,但方便關聯:
#
指向搜尋的標的,後接search()browse()等方法
myrec_orm = self.env['res.partner'].search([])
#
顯示指向的標的,res.partner(3, 1)表示指向的id(Primary Key)
myrec_orm
#
顯示指向的每一筆資料,active欄位為True,該筆資料才會啟用
for rec in myrec_orm:
    rec.name, rec.display_name, rec.active
l   SQL是選取出來的資料,速度較快,但因已無鏈列而不方便關聯:
#
指向搜尋的標的
self.env.cr.execute("select name, display_name, active from res_partner")
myrec_sql = self.env.cr.fetchall()
#
顯示指向的標的
myrec_sql
# SQL
指令下,active欄位為False的資料也會顯示
for rec in myrec_sql:
    rec[0], rec[1], rec[2]
l   Odoo ORM可以選取計算的欄位:
myrec_orm = self.env['openacademy.score'].search([])
myrec_orm
for rec in myrec_orm:
    rec.score_student.student_name, rec.score_total, rec.score_avg
l   SQL不可以選取計算的欄位:
#
這會失敗,因為計算的欄位不存在於資料庫
self.env.cr.execute("select score_student, score_total, score_avg from openacademy_score")
l   使用Odoo ORM API新增資料:
myrec = self.env['openacademy.student'].search([])
myrec.create({'student_no': 'S001', 'student_name': 'Peter', 'student_class': '1'})
#
執行指令,只需在Shell中使用,或exit()後自動執行指令
self.env.cr.commit()
l   使用Odoo ORM API修改資料:
myrec = self.env['openacademy.student'].search([('student_no', '=', 'S001')])
#
確保選到正確的資料
myrec.student_no
myrec.write({'student_name': 'Timmy'})
self.env.cr.commit()
l   使用Odoo ORM API刪除資料:
myrec = self.env['openacademy.student'].search([('student_no', '=', 'S001')])
myrec.student_no
myrec.unlink()
self.env.cr.commit()
l   使用Odoo ORM API操作紀錄集:
myrec = self.env['res.partner'].search([])
len(myrec)
myrec.ids
myrec.mapped('name')
myrec.mapped(lambda rec: (rec.id, rec.name))
myrec.sorted(key=lambda rec: rec.id, reverse=True)

myrec_2 = myrec.filtered(lambda rec: rec.name.startswith('s'))
myrec_2
l   使用Odoo ORM API操作日期和時間:
from odoo import fields
import datetime

#
將字串轉換為datedatetime
fields.Datetime.from_string('2018-07-26 08:56:27')
#
datedatetime轉換為字串
fields.Datetime.to_string(fields.Datetime.now())

fields.Date.today()
fields.Datetime.now()

#
複製一個已有的紀錄
myrec = self.env.ref('base.user_root')
myrec_2 = myrec.copy({'name': 'admin'})

#
返回該筆紀錄的日期
fields.Date.context_today(myrec, timestamp=None)
fields.Datetime.context_timestamp(myrec_2, timestamp=datetime.datetime.now())
l   更多ORM API用法:
self.env.user
目前使用者的紀錄
self.env.uid
目前使用者的ID,等於self.env.user.id
self.env.context
系統語系等紀錄
self.env.cr.savepoint()
設置一個保存點,在rollback時使用
self.env.cr.rollback()
取消目前的更新操作,rollback到上次設置的保存點

Odoo ORM API - One2manyMany2many字段專用的操作方式
l   One2manyMany2many字段專用的特殊格式:
(
模式, 子資料id(Primary Key),需進資料庫查看, 資料內容)
l   模式0-在特定的母資料新增子資料:
myrec = self.env['openacademy.studentclass'].search([('studentclass_name', '=', 'C001')])
myrec.studentclass_name
myrec.write({'studentclass_line': [(0, _, {'student_no': 'S002', 'student_name': 'Stanley', 'student_class': '3'})]})
self.env.cr.commit()
l   模式1-在特定的母資料修改特定的子資料:
myrec = self.env['openacademy.studentclass'].search([('studentclass_name', '=', 'C001')])
myrec.studentclass_name
myrec.write({'studentclass_line': [(1, 3, {'student_contact': 'LAN-SIR'})]})
self.env.cr.commit()
l   模式2-在特定的母資料刪除特定的子資料:
myrec = self.env['openacademy.studentclass'].search([('studentclass_name', '=', 'C001')])
myrec.studentclass_name
myrec.write({'studentclass_line': [(2, 3, _)]})
self.env.cr.commit()
l   模式3-在特定的母資料取消關聯特定的子資料
(
子資料不能設定為cascade,否則不是取消關聯而是刪除)
myrec = self.env['openacademy.studentclass'].search([('studentclass_name', '=', 'C001')])
myrec.studentclass_name
myrec.write({'studentclass_line': [(3, 4, _)]})
self.env.cr.commit()
l   模式4-在特定的母資料新增特定的子資料:
myrec = self.env['openacademy.studentclass'].search([('studentclass_name', '=', 'C001')])
myrec.studentclass_name
myrec.write({'studentclass_line': [(4, 1, _)]})
self.env.cr.commit()
l   模式5-在特定的母資料刪除所有的子資料:
myrec = self.env['openacademy.student'].search([('student_no', '=', 'S001')])
myrec.student_no
myrec.write({'student_course': [(5, _, _)]})
self.env.cr.commit()
l   模式6-在特定的母資料替換所有的子資料:
myrec = self.env['openacademy.student'].search([('student_no', '=', 'S001')])
myrec.student_no
myrec.write({'student_course': [(6, _, [2, 3])]})
self.env.cr.commit()

i18n資料夾-翻譯文件po
l   Step 1啟動欲翻譯的語言:
Settings > Activate the developer mode
Translations > Languages > Active
欲翻譯的語言 > Update Terms > Load


l   Step 2匯出欲翻譯語言的po檔:
Translations > Export Translation >
Language
選擇欲翻譯的語言,
File Format
選擇PO File
Apps To Export
選擇翻譯的模組 > Export > 點選po檔,此例為zh_TW.po


l   Step 3將翻譯內容填寫至po檔的msgstrmsgid則是原文內容。


l   Step 4將翻譯完成的po檔匯入系統,客製化開發模組的po檔要放在i18n資料夾(po檔檔名都不可以修改)
Translations > Import Translation >
填寫Language NameCode,匯入File > Import


l   Step 5變更使用者使用的語言,並重新登入系統:
Users & Companies > Users > Preferences > Language


wizards資料夾-彈出式視窗精靈
l   習慣上,wizards資料夾是一個獨立的Package,所有wizards相關檔案皆放置於此。
l   Step 1openacademy資料夾內修改__init__.py
#
原程式碼增加以下程式碼
from . import wizards
l   Step 2wizards資料夾內創建openacademy_student_search_wizard.py
# -*- coding: utf-8 -*-
# Author: Peter Wu

from odoo import models, fields, api
from odoo.exceptions import UserError

# models.TransientModel
用在臨時欄位
class openacademystudentsearchwizard(models.TransientModel):
    _name = "openacademy.student_search_wizard"
   
    student_name = fields.Char(string = '
學生姓名')
    student_class = fields.Selection([('1', '
一年級'), ('2', '二年級'), ('3', '三年級')], string = '年級')
    student_fm = fields.Selection([('M', '
'), ('F', '')], string = '性別')
   
    def run_student_search(self):
        #
得到的domain格式為[('', 'ilike', ''), ('', '=', ''), ('', '=', '')]
        domain = []
        if self.student_name:
            domain.append(('student_name', 'ilike', self.student_name))
        if self.student_class:
            domain.append(('student_class', '=', self.student_class))
        if self.student_fm:
            domain.append(('student_fm', '=', self.student_fm))
        if not domain:
            domain = [(1, '=', 1)]
       
        myviewid = self.env.ref('openacademy.view_openacademy_student_tree')
       
        return {'view_name': 'openacademystudentwizard',
                'name': ('Openacademy Student Data'),
                'views': [[False, 'tree'], [False, 'form']],
                'res_model': 'openacademy.student',
                'context': self._context,
                'type': 'ir.actions.act_window',
                'view_id': myviewid.id,
                # target
selfmain選項,self於頁面上方保留歷程
                'target': 'main',
                'domain': domain,
                'flags': {'action_buttons': True},
                'view_mode': 'form',
                'view_type': 'form'
                }
l   Step 3wizards資料夾內創建__init__.py
# -*- coding: utf-8 -*-
# Author: Peter Wu

from . import openacademy_student_search_wizard
l   Step 4wizards資料夾內創建openacademy_student_search_wizard.xml
<?xml version="1.0" encoding="utf-8"?>
<odoo>
    <data>
        <record id="openacademy_student_form_wizard" model="ir.ui.view">
            <field name="name">openacademy.student.form.wizard</field>
            <field name="model">openacademy.student_search_wizard</field>
            <field name="arch" type="xml">
                <form string="">
                    <sheet>
                        <h1>
學生資料複合查詢</h1>
                        <group>
                            <field name="student_name"/>
                            <field name="student_class"/>
                            <field name="student_fm"/>
                        </group>
                    </sheet>
                    <footer>
                        <button type="object" name="run_student_search" string="
查詢" class="oe_highlight"/>
                        <button special="cancel" string="
離開"/>
                    </footer>
                </form>
            </field>
        </record>
        <act_window id="action_openacademy_student_search_view" name="
學生資料複合查詢" res_model="openacademy.student_search_wizard" src_model="" view_mode="form" target="new" multi="False"/>
    </data>
</odoo>
l   Step 5openacademy資料夾內修改__manifest__.py
'data': [
    'wizards/openacademy_student_search_wizard.xml',
],
l   Step 6views資料夾內修改openacademy_menu.xml
<!--
原程式碼增加以下程式碼-->
<menuitem id="menu_openacademy_search_categ"
    name="
複合查詢"
    parent="menu_openacademy_root"
    sequence="30"/>
<menuitem id="menu_openacademy_student_search_view"
    name="" parent="menu_openacademy_search_categ"
    action="action_openacademy_student_search_view"
    sequence="10"/>

權限劃分-表單檢視權限不同
l   Step 1security資料夾內創建openacademy_menu_rule.xml
因為已將setting.jar匯入Live templates,可使用編輯技巧-
輸入odoo_data再按Tab鍵、
輸入odoo_security_category再按Tab鍵、
輸入odoo_security_group再按Tab鍵:

<?xml version="1.0" encoding="utf-8"?>
<odoo>
    <data>
        <record id="security_openacademy_categ" model="ir.module.category">
            <field name="name">
學生管理權限</field>
            <!--eval
常用於計算,例如計算昨天<field name="change_date" eval="(dateime.now()+timedelta(-1)).strftime('%Y-%m-%d')"/>-->
            <field name="sequence" eval="1"/>
        </record>
        <record id="group_openacademy_1" model="res.groups">
            <field name="name">
學生</field>
            <field name="category_id" ref="security_openacademy_categ"/>
            <!--Odoo ORM API
的模式4-->
            <field name="implied_ids" eval="[(4, ref('base.group_user'))]"/>
        </record>
        <record id="group_openacademy_2" model="res.groups">
            <field name="name">
教員</field>
            <field name="category_id" ref="security_openacademy_categ"/>
            <field name="implied_ids" eval="[(4, ref('group_openacademy_1'))]"/>
        </record>
        <record id="group_openacademy_3" model="res.groups">
            <field name="name">
校長</field>
            <field name="category_id" ref="security_openacademy_categ"/>
            <field name="implied_ids" eval="[(4, ref('group_openacademy_2'))]"/>
        </record>
    </data>
</odoo>
l   Step 2openacademy資料夾內修改__manifest__.py
'data': [
    'security/openacademy_menu_rule.xml',
],
l   Step 3views資料夾內修改openacademy_menu.xml
<!--
增加groups,將欲對應的record idmenuitem groups關聯起來-->
<?xml version="1.0" encoding="utf-8"?>
<odoo>
    <data>
        <menuitem id="menu_openacademy_root"
            name="
學生成績系統"
            sequence="200"/>
        <menuitem id="menu_openacademy_categ"
            name="
基礎資料"
            parent="menu_openacademy_root"
            sequence="10"/>
        <menuitem id="menu_openacademy_student_view"
            name="" parent="menu_openacademy_categ"
            groups="group_openacademy_1"
            action="action_openacademy_student_view"
            sequence="10"/>
        <menuitem id="menu_openacademy_teacher_view"
            name="" parent="menu_openacademy_categ"
            groups="group_openacademy_3"
          
 action="action_openacademy_teacher_view"
            sequence="20"/>
        <menuitem id="menu_openacademy_studentclass_view"
            name="" parent="menu_openacademy_categ"
            groups="group_openacademy_2"
           
action="action_openacademy_studentclass_view"
            sequence="30"/>
        <menuitem id="menu_openacademy_course_view"
            name="" parent="menu_openacademy_categ"
            groups="group_openacademy_3"
       
    action="action_openacademy_course_view" sequence="40"/>
        <menuitem id="menu_openacademy_score_categ"
            name="
成績紀錄"
            parent="menu_openacademy_root"
            sequence="20"/>
        <menuitem id="menu_openacademy_score_view"
            name="" parent="menu_openacademy_score_categ"
            groups="group_openacademy_1"
          
 action="action_openacademy_score_view"
            sequence="10"/>
        <menuitem id="menu_openacademy_search_categ"
            name="
複合查詢"
            parent="menu_openacademy_root"
            sequence="30"/>
        <menuitem id="menu_openacademy_student_search_view"
            name="" parent="menu_openacademy_search_categ"
            action="action_openacademy_student_search_view"
            sequence="10"/>
    </data>
</odoo>
l   Step 4創建新的使用者來驗證:
Settings > Users & Companies > Users > Create >
設定Application AccessesNotification Management Handle in Odoo >
完成後回到Users,左邊核取方塊打勾 > Action > Change Password


權限劃分-繼續設定,資料檢視權限不同(員工、部門)
l   Step 1此功能需事先安裝Employee模組,在models資料夾內修改openacademy_student.py
#
原程式碼增加以下程式碼
student_employee = fields.Many2one('hr.employee', string = 'Employee')
l   Step 2views資料夾內修改openacademy_student.xml
<!--
原程式碼treeform皆增加以下程式碼-->
<field name="student_employee"/>
l   Step 3security資料夾內創建openacademy_student_rule.xml
因為已將setting.jar匯入Live templates,可使用編輯技巧-
輸入odoo_data再按Tab鍵、
輸入odoo_security_rule再按Tab鍵:

<?xml version="1.0" encoding="utf-8"?>
<odoo>
    <data>
        <!--
學生權限,依學生帳號不同,檢視學生自己的資料-->
        <record id="openacademy_student_rule1" model="ir.rule">
            <field name="name">openacademy.student.rule1</field>
            <field ref="model_openacademy_student" name="model_id"/>
            <field name="domain_force">[('student_employee.id', '=', user.employee_ids.id )]</field>
            <field name="groups" eval="[(4, ref('openacademy.group_openacademy_1'))]"/>
        </record>
        <!--
教員權限,依教員部門不同,檢視所屬部門學生的資料-->
        <record id="openacademy_student_rule2" model="ir.rule">
            <field name="name">openacademy_student_rule2</field>
            <field ref="model_openacademy_student" name="model_id"/>
            <field name="domain_force">[('student_employee.department_id.id', 'in', [x.department_id.id for x in user.employee_ids])]</field>
            <field name="groups" eval="[(4, ref('openacademy.group_openacademy_2'))]"/>
        </record>
        <!--
校長權限,擁有所有資料檢視權限-->
        <record id="openacademy_student_rule3" model="ir.rule">
            <field name="name">openacademy_student_rule3</field>
            <field ref="model_openacademy_student" name="model_id"/>
            <field name="groups" eval="[(4, ref('openacademy.group_openacademy_3'))]"/>
            <field name="global" eval="1"/>
        </record>
    </data>
</odoo>
l   Step 4openacademy資料夾內修改__manifest__.py
'data': [
    'security/openacademy_student_rule.xml',
],
l   Step 5security資料夾內創建openacademy_score_rule.xml
<?xml version="1.0" encoding="utf-8"?>
<odoo>
    <data>
        <!--
學生權限,依學生帳號不同,檢視學生自己的資料-->
        <record id="openacademy_score_rule1" model="ir.rule">
            <field name="name">openacademy.score.rule1</field>
            <field ref="model_openacademy_score" name="model_id"/>
            <field name="domain_force">[('score_student.student_employee.id', '=', user.employee_ids.id )]</field>
            <field name="groups" eval="[(4, ref('openacademy.group_openacademy_1'))]"/>
        </record>
        <!--
教員權限,依教員部門不同,檢視所屬部門學生的資料-->
        <record id="openacademy_score_rule2" model="ir.rule">
            <field name="name">openacademy.score.rule2</field>
            <field ref="model_openacademy_score" name="model_id"/>
            <field name="domain_force">[('score_student.student_employee.department_id.id', 'in', [x.department_id.id for x in user.employee_ids])]</field>
            <field name="groups" eval="[(4, ref('openacademy.group_openacademy_2'))]"/>
        </record>
        <!--
校長權限,擁有所有資料檢視權限-->
        <record id="openacademy_score_rule3" model="ir.rule">
            <field name="name">openacademy.score.rule3</field>
            <field ref="model_openacademy_score" name="model_id"/>
            <field name="groups" eval="[(4, ref('openacademy.group_openacademy_3'))]"/>
            <field name="global" eval="1"/>
        </record>
    </data>
</odoo>
l   Step 6openacademy資料夾內修改__manifest__.py
'data': [
    'security/openacademy_score_rule.xml',
],
l   Step 7關聯Odoo系統使用者與Employee模組來驗證學生員工權限:
Employees > Employees > Create > HR Setings > Related User > Save
新增的student_employee欄位也要先關聯好資料,學生登入系統時僅能檢視學生自己的資料。


l   Step 8關聯Odoo系統使用者與Employee模組來驗證教員部門權限:
Employees > Departments > Create > Save
Employees > Employees > Edit > Work Information > Department
讓多位學生與一位教員放在同一個部門,教員登入系統時僅能檢視所屬部門學生的資料。

沒有留言:

張貼留言