odoo server.action that (can’t)redirect ?
ผมได้รับโจทย์มาว่าต้องการให้หน้าระบบ odoo มีเมนูเพิ่ม
ให้อยู่ที่เดียวกับ action menu
ที่ใครหลายคนน่าจะเคยเอาไว้ใช้กันหลากหลายสถานการณ์
ไม่ว่าจะเป็น
- Print report
- Duplicate
- Delete
ทีนี้โจทย์ที่ได้รับมาคือต้องเอา record(s) โยนเข้าไปในฟังก์ชันแล้วก็ไปผ่าน process
โดย input คือ ตั้งแต่ 1…n record(s) แล้วนำข้อมูลทั้งหมดมารวมกันไว้ใน record เดียว
แต่ให้อยู่ในรายการ (line) ใน model ใหม่แทน
Output ของมันคือการ create new record ซึ่ง…เป็นคนละ view และก็เป็นคนละ model กันด้วย
ถ้าใครเคย implement โจทย์ประมาณนี้น่าจะพอนึกออกว่า เมื่อ create เรียบร้อยแล้ว
เราควร redirect user ไปหาหน้า view ที่ต้องการเลยก็จะเป็นเรื่องที่ดีกว่า
ในแง่ของ User Experience
ซึ่งท่าปกติที่ทำกันก็น่าจะประมาณนี้
@api.model
def action_create_new_record(self, selected_data):
# do the rest
# ...
return {
'type': 'ir.actions.act_window',
'name': 'Existing view',
'res_model': 'existing.model.name',
'view_mode': 'form',
'res_id': res.id,
'target': 'current',
}
ซึ่งถ้า implement ด้วยท่าประมาณนี้ ด้วยการใช้ Button หรือจาก Wizard ก็ดูจะไม่มีปัญหาอะไร แต่… พอเอามาใช้กับ Server action มันดันไม่ได้ผลลัพธ์ตามที่ต้องการนี่ล่ะสิ…
ตัวอย่างโค้ดที่หน้า View ที่จะสั่งให้ส่ง record ไปหาฟังก์ชันที่เขียนไว้ที่ py file
<record id="action_to_create_new_record" model="ir.actions.server">
<field name="name">Create New Record</field>
<field name="model_id" ref="model_existing_model_name"/>
<field name="binding_model_id" ref="my_module.model_existing_model_name"/>
<field name="binding_view_types">form,list</field>
<field name="state">code</field>
<field name="code">
model.action_create_new_record(env.context.get('active_ids', []));
</field>
</record>
ไหน ๆ ก็แปะโค้ดชุดนี้มาแล้ว ขอแวะอธิบายแต่ละบรรทัด ทวนกันหน่อย
1. Block ใหญ่สุด record ครอบเอาไว้ โดยใส่ attribute เป็น model='ir.actions.server'
คือจะให้โค้ด block นี้เป็นการเรียกใช้ Server Action
ส่วน id ก็เป็นอะไรก็ได้ตามแต่เราต้องการ
ซึ่งถ้าแนวทางเบื้องต้นก็ตั้งชื่อให้สื่อถึงการทำงานของมันเอาไว้ก็ก่อนก็ได้ครับ
<record id="action_to_create_new_record" model="ir.actions.server">
.
.
</record>
2. ถัดมาเป็นส่วนของ name=name
ส่วนนี้ก็ตรงตัวแหละ คือชื่อเมนูนั่นเอง
<field name="name">Create New Record</field>
3. ส่วนถัดมาเป็นชื่อ model ที่เราจะไปเรียกฟังก์ชัน โดยในที่นี้ผมสมมติว่า
เรามี model อยู่ตัวนึงที่ชื่อว่า existing.model.name
แล้วกัน
<field name="model_id" ref="model_existing_model_name"/>
4. ส่วนนี้จะเป็นว่าเราจะ binding การทำงานของ action นี้ไปที่ไหนบ้าง
คือเราจะให้เวลาที่กด action แล้วจะให้เมนู Create New Record ไปโชว์อยู่หน้าจอไหนบ้างนั่นแหละ
ซึ่งในที่นี้ผมขอเอาไว้ 2 จุด คือ อยู่ทั้งหน้า list และหน้า form ใน model เดิมที่จะเรียกฟังก์ชันนี้
<field name="binding_model_id" ref="my_module.model_existing_model_name"/>
<field name="binding_view_types">form,list</field>
5. ส่วนสุดท้าย คือจะเป็น tag ที่เราเอาไว้ใช้ในกรณีที่ต้องการสั่ง execute code
ที่อยู่ภายใต้ model
เราก็จะใช้เป็น model.action_create_new_record
เรียกไปที่ฟังก์ชันที่อยู่ภายใต้ model ในข้อ 3 | model_id
ได้เลย
ซึ่งจากโจทย์ที่บอกไว้ในตอนแรก คือผมต้องรวม record ตั้งแต่ 1…n แล้วไปสร้างรวมกันไว้ที่อีก model นึง
แล้วค่อยส่ง user ไปหาหน้า form ของ record ใหม่ที่เพิ่งสร้างขึ้นมา
ก็เลยเป็นเหตุผลว่า ผมส่งข้อมูลเข้าไปที่ฟังก์ชันเป็น env.context.get('active_ids')
ก็เพื่อให้ทุก record ที่เลือก ได้ผ่านเข้าไปที่ function นี่แหละ
<field name="state">code</field>
<field name="code">
model.action_create_new_record(env.context.get('active_ids', []));
</field>
จบการอธิบายเรื่อง server.action tag (ยืดยาวกันเลยทีเดียว >.<)
ซึ่งหลังจากที่ลองไปลองมาหลาย ๆ แบบ
ทั้งลอง return ไปที่ block code ใน xml (ซึ่งทำไม่ได้) และเรียกฟังก์ชันไปหลายรูปแบบ
ลามไปจนคิดว่าทำ binding ผิดที่ หรือไม่ก็เรียก model_id แบบนี้ไม่ได้ (ออกทะเลไปไกลมาก 5555)
สุดท้าย… ก็มาจบที่โค้ดชุดนี้
<record id="action_to_create_new_record" model="ir.actions.server">
<field name="name">Create New Record</field>
<field name="model_id" ref="model_existing_model_name"/>
<field name="binding_model_id" ref="my_module.model_existing_model_name"/>
<field name="binding_view_types">form,list</field>
<field name="state">code</field>
<field name="code">
action = model.action_create_new_record(env.context.get('active_ids', []));
</field>
</record>
จะเห็นได้ว่า จุดต่างมีแค่นิดเดียวเลย… action = ...
ใช่ครับ… แค่ assign ตัวแปรเข้ามานี่แหละ จุดเปลี่ยนของเรื่องนี้เลย
return statement ที่เป็นการส่ง user ให้ redirect ไปที่หน้า form view ก็ใช้ได้ซะยังงั้นไปเลย
เมื่อเจอปัญหานี้ก็เลยได้ไปหาข้อมูลเพิ่มเติมจนได้ความว่า ถ้าเราเรียก block code ใน server action
- odoo จะรอรับ data dict ที่ส่งกลับมาจาก field
code
ของ server.action - แต่ถ้าเราไม่ได้ assign ตัวแปรเอาไว้ใน block นี้ มันก็จะ capture อะไรไม่ได้
<field name="code">
model.action_create_new_record(env.context.get('active_ids', []));
</field>
- ก็เลยเป็นที่มาว่า เมื่อเรา assign variable ให้ฟังก์ชันของเราไปแบบนี้
<field name="code">
action = model.action_create_new_record(env.context.get('active_ids', []));
</field>
- server.action ก็จะ redirect ไปได้ ตามที่เราต้องการ
ไม่ต่างกับการ redirect ผ่าน Button หรือ Wizard แล้ว
Lesson learned !!
ส่วนถ้าใครพอจะท่าอื่นที่ง่ายกว่านี้ หรือเคยทำแบบไหน แล้วเวิกไม่เวิก ก็คุยกันมาได้นะคร้าบบบ
>_JV