Print report ทั้งที ขอเก็บเป็นไฟล์เอาไว้ด้วยสิ ?
ปัญหา ?
- อยากเก็บไฟล์ pdf ให้เป็นข้อมูลตามวันที่สั่ง print
- สามารถทำเป็น version เก็บไว้เพื่อ download ได้ในภายหลัง
ใครหลายคนคงคุ้นเคยกับการ print report ผ่านระบบ odoo กันอยู่แล้ว
แต่คงจะเคยประสบปัญหากันว่า Print ออกมาแล้วข้อมูลเปลี่ยนไป…
หรือบาง Business ก็อาจจะเป็นความต้องการว่า
อยากเก็บ log ของเอกสารที่มีการ print ออกไปจากระบบก็ตามที…
แต่ครั้นว่าเราจะต้องไปเขียนดักไว้ในทุกๆ model ที่ต้องการเก็บ log เอกสารไว้
ก็ดูจะลำบากในการดูแลอยู่พอสมควร ทีนี้จะทำยังไงดีล่ะ??
repo reference : https://github.com/zhephirothz/odoo_print_pdf_to_attachment
Challenge begin!!
ด้วยความที่ odoo นั้น ถูกออกแบบฟังก์ชันในการ print report ให้เป็นการนำข้อมูล ณ ปัจจุบัน
ส่งไปผ่าน report engine (wkhtmltopdf) ซึ่งจะเป็นการส่งข้อมูลไป print ออก pdf ใหม่ ทุกครั้ง
เพราะฉะนั้นแล้ว ถ้ามองในแง่ของความเที่ยงตรงของไฟล์ที่ได้มาจาก record นั้นๆ
ที่เราไปสั่ง print ออกมาในแต่ละครั้ง
ในเงื่อนไขนี้ จริงๆแล้ว เราก็จะต้องถือว่า pdf ที่ออกมาในแต่ละครั้ง
อาจจะไม่เหมือนกันก็ได้ ขึ้นอยู่กับข้อมูล ณ ขณะนั้น
.
ซึ่งเรื่องนี้จริง ๆ แล้ว ถ้าเป็นข้อกังวลของผู้ใช้งาน เราก็อาจจะแก้ไขด้วยหลากหลายแนวทาง
- การล็อคข้อมูล เมื่อเอกสารถึงสถานะใดสถานะหนึ่ง
- วาง Business flow ให้ข้อมูลไม่มีการเปลี่ยนแปลงไปได้ในช่องข้อมูลที่ค่อนข้าง sensitive
แต่… ก็นั่นแหละครับ
ถึงจะว่ามีการล็อคข้อมูลสารพัดวิธี หรืออะไรก็ตามที
แต่ในทางปฏิบัติ ก็อาจจะมีบางครั้งที่เราจำเป็น หรือไม่ก็จำยอม
ที่จะต้องแก้ไขข้อมูล
โดยการ force database ตรง ๆ
หรือไม่ก็เป็นสิทธิ์ของ superuser ที่สามารถแก้ไขได้
(ซึ่งถ้าเลี่ยงไม่ให้เกิดเคสนี้ขึ้นได้ก็จะดีกว่านะ -0-)
.
แล้ว pdf file ที่สั่ง print ไปแล้วล่ะ?
แน่นอนสิครับ ว่าถ้ามาสั่ง print อีกครั้งหลังจากที่ข้อมูลแก้ไขไปแล้ว ข้อมูลในนั้นก็จะถูกเปลี่ยนตาม
.
ซึ่ง… ถ้าเป็นการแก้ไขข้อมูลที่ไม่ได้มีความละเอียดอ่อนเท่าไร ก็คงไม่ได้เป็นปัญหาอะไร
และ blog นี้ก็คงจะไม่ต้องพูดถึงประเด็นนี้ด้วย (55555 xD)
แต่ถ้าเป็นการแก้ไขข้อมูลที่ค่อนข้าง “Sensitive” ล่ะ ?
ปัญหาจะเกิดก็ตรงนี้แหละ…
- เกิดอะไรขึ้น ?
- ใครแก้ข้อมูล ?
- แก้ส่วนไหนไปบ้าง ?
- ข้อมูลเก่าคืออะไร ?
ซึ่งถ้าระบบทำเรื่อง log ไว้ครอบคลุมก็อาจจะตามกันเจอโดยไม่ได้ยากนัก
แล้วไงต่อ ?
ทีนี้… ในเมื่อทุก ๆ การสั่ง print ที่ผ่านหน้าระบบ odoo จะต้องผ่าน library อยู่แล้ว
ถ้างั้นเราก็ดักไว้ระหว่างกลาง แล้วพอเราได้ข้อมูลไฟล์ pdf มา
เราก็เอาไปแปะไว้ใน attachment ของ records นั้น ๆ ไปได้ด้วยเลยซะสิ
ซึ่งสำหรับ qweb report ของ odoo นั้น
ส่วนที่พวกเราน่าจะคุ้นเคยกันอยู่ก็น่าจะเป็นที่ model นี้
ir.actions.report
.
ทีนี้ถ้าเราไปไล่ดูฟังก์ชันของ core odoo ว่าถ้าจะสั่ง print pdf มันควรจะต้องไปดักที่ไหนได้
ก็ลองไปดูได้ตามนี้ odoo/addons/base/models/ir_actions_report.py
.
.
ถ้าลองไล่โค้ดดู ก็จะเห็นได้ว่าฟังก์ชันที่ใช้ในการสั่ง print จะเข้ามาที่
_render_qweb_pdf()
ซึ่งถ้าเมื่อไล่ดูโค้ดในนั้นเราก็จะเจอว่า
จริง ๆ แล้วมันมีส่วนที่เอาไว้สร้างและดึงข้อมูลจาก attachment ไฟล์ไว้อยู่แล้ว
จะเป็นโค้ดใน if statement ที่อยู่ในส่วนนี้
def _render_qweb_pdf(self, report_ref, res_ids=None, data=None):
# .
# ...
# Generate the ir.attachment if needed.
if not has_duplicated_ids and report_sudo.attachment and not self._context.get("report_pdf_no_attachment"):
attachment_vals_list = self._prepare_pdf_report_attachment_vals_list(report_sudo, collected_streams)
if attachment_vals_list:
attachment_names = ', '.join(x['name'] for x in attachment_vals_list)
try:
self.env['ir.attachment'].create(attachment_vals_list)
except AccessError:
_logger.info("Cannot save PDF report %r attachments for user %r", attachment_names, self.env.user.display_name)
else:
_logger.info("The PDF documents %r are now saved in the database", attachment_names)
ถ้าจะบอกว่าระบบสามารถ generate attachment ไฟล์มาแนบได้อยู่แล้ว ก็ไม่ผิดสักเท่าไรนัก
ถ้าอยากรู้ว่าวิธีการเป็นแบบไหน และเราทำอะไรกับฟังก์ชันส่วนนี้ได้บ้าง
ไปตามกันต่อได้ที่ devlogs ถัดไปนะครับ xD >>
ขอสปอยกันก่อนว่า จากโจทย์ของเรา คือ
ต้องการให้สามารถ track เป็น logs ได้ด้วย ว่าใครเป็นคนที่สั่ง print
การทำเป็นไฟล์แนบแบบที่ core function ทำงานได้ เลยจะยังไม่ตอบโจทย์สักเท่าไรนัก
Time to code!!
จุดที่เราจะไปสนใจกันในวันนี้ จะเป็นฟังก์ชันที่ชื่อ
_render_qweb_pdf_prepare_streams()
ตรงนี้นี่แหละที่เราจะมาเล่นกัน
จากโจทย์ เรามีสิ่งที่จะต้องทำเพิ่มอยู่อีก 1 อย่างที่ ยังขาดไปใน core function
- ต้องรู้ user ว่าใครเป็นคน print เอกสารเลขไหน และสามารถเห็นข้อมูลนี้ได้ทุกคนที่มีสิทธิ์ในการเข้าถึง record นั้น ๆ
.
.
ทีนี้ผมก็เลยคิดว่าถ้างั้นเราก็ให้ทุก ๆ ครั้งที่มีการ generate report
ก็เขียนเพิ่มเข้าไปให้ไปเอา attachment ที่สร้างมาใหม่ไปแปะไว้ใน chatter ซะเลยสิ
เพราะ chatter จะระบุชื่ออยู่แล้ว ว่าชื่อ user ไหนเป็นที่ execute คำสั่งนั้น
records.message_post()
สุดท้ายก็เลยได้โค้ดที่เขียน inherit เพิ่มเข้าไปเป็นแบบนี้
def _render_qweb_pdf_prepare_streams(self, report_ref, data, res_ids=None):
res = super()._render_qweb_pdf_prepare_streams(report_ref, data, res_ids)
model_name = data['context']['active_model']
# Save the PDF as an attachment for each res_id and post it to Chatter
if res_ids:
for res_id in set(res_ids):
pdf_stream = res[res_id]['stream']
# Reset the stream pointer to the beginning
pdf_stream.seek(0)
pdf_content = pdf_stream.read()
record = self.env[model_name].browse(res_id)
# Construct the attachment name
attachment_name = f"{record.name}_{datetime.now().strftime("%Y-%m-%d_%H-%M-%S")}.pdf"
# Create the attachment
attachment = self.env['ir.attachment'].create({
'name': attachment_name,
'type': 'binary',
'datas': base64.b64encode(pdf_content),
'mimetype': 'application/pdf',
'res_model': model_name,
'res_id': res_id,
})
# Post the attachment to the Chatter
record.message_post(
body=f"Generated report: {attachment_name}",
attachment_ids=[attachment.id],
)
return res
Github repo ของ module นี้ : https://github.com/zhephirothz/odoo_print_pdf_to_attachment
ซึ่งในที่นี้ ผมจะใช้ชื่อไฟล์ให้มัน unique ไปเลยด้วยการเอา
ชื่อrecord มาต่อกับ datetime.now() ไปเลย
ถ้าใครมี requirement ที่นอกเหนือไปจากนี้ก็
แก้ไขในส่วนของ attachment_name
เอาได้เลยนะครับ
.
.
ก็เพียงเท่านี้เราก็จะแก้ปัญหาที่เจอได้เรียบร้อย
- มี Log การ print เอกสาร
- เห็นว่า User คนไหนเป็นคน print รู้ไปถึงว่าเป็นไฟล์ไหนเลยอีกด้วย
ก็หลังจากที่ลองหาทาง adadpt กับ core function อยู่สักพัก
โจทย์นี้ก็เลยมาจบตรงที่เขียน inherit เพิ่มไปอีกนิดนึงที่ฟังก์ชัน print เลย
เป็นอันว่าจบบบบ
Lesson Learned
.
.
** Note เพิ่มไว้หน่อย ถ้าไปในท่านี้ จริง ๆ ก็มี trade off อยู่พอสมควรเหมือนกัน
1. เรื่องแรกเลย High storage capacity กินพื้นที่สูงมากกกกก
ก็การกด print 1 ครั้ง เซฟไฟล์เข้า filestore 1 ครั้ง
ถ้าเป็น custom report ที่มีขนาดของไฟล์ค่อนข้างใหญ่ด้วย ก็อย่าลืม concern เรื่องนี้ด้วย
2. การทำไว้ในทุก ๆ model อาจจะไม่เหมาะ
เพราะบาง model ก็ไม่ได้จำเป็นจะต้องเก็บ logs เอกสารไว้ละเอียดขนาดนั้น
อาจจะไม่ได้จำเป็นที่จะต้องรู้ทุก change ขนาดนั้น
ซึ่งส่วนนี้ถ้าจะทำ model filter เอาไว้ก็ได้
ก็เพิ่มเงื่อนไขเข้าไปหน่อยตอนจังหวะดึง `model_name` มา
disclaimer
- แนวทางแก้ปัญหาเรื่องเหล่านี้ ขึ้นอยู่กับ Business และ Challenge ที่พบเจอ ไม่ได้เป็น one size fit all แต่อย่างใด เพราะฉะนั้นต้องประเมินความเหมาะสมก่อนนำวิธีการเหล่านี้ไปใช้กับงานของเพื่อน ๆ นะคร้าบบบ
- โค้ดที่แปะไว้ จะใช้กับ standard module ได้เป็นหลักนะครับ เพราะจะเห็นได้ว่า `attachment name` ผมใช้เป็น `record.name` ซึ่งหมายถึงว่า ถ้าจะเอาไปใช้กับ custom module ด้วย ก็ต้อง make sure หน่อยว่ามี field ชื่อ `name` อยู่ในนั้นด้วย (หรือจริง ๆ ดักเงื่อนไขไว้ก่อนก็ได้แหละ xD)
*** odoo version ที่ใช้เป็นตัวอย่างในบทความนี้เป็น version 18.0 CE ครับ