Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions digital_signature_kmitl/README.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
=======================
Digital Signature KMITL
=======================

Per-user digital signature storage and capture for KMITL approval workflows.

Features
========

* Store personal digital signature on each user profile (``res.users.signature_image``)
* Signature widget in user form and preferences for easy drawing/upload
* QWeb helper template for consistent PDF signature block rendering
* Foundation module for approval document signature capture

Usage
=====

#. Install module: ``digital_signature_kmitl``
#. Go to Settings > Users & Companies > Users
#. Open your user profile
#. Go to "Preferences" tab → "ลายเซ็นดิจิตอล" section
#. Draw or upload your signature
#. Save

Contributors
============

* Aginix Technologies
1 change: 1 addition & 0 deletions digital_signature_kmitl/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from . import models as models # noqa: F401
19 changes: 19 additions & 0 deletions digital_signature_kmitl/__manifest__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{
"name": "Digital Signature Kmitl",
"version": "16.0.1.0.0",
"summary": "Per-user digital signature stored on res.users, with QWeb helper for PDF reports.",
"category": "KMITL",
"author": "Aginix Technologies",
"website": "https://github.com/aginix/kmitl",
"depends": [
"base",
"web",
],
"data": [
"views/res_users_views.xml",
"views/signature_templates.xml",
],
"installable": True,
"auto_install": False,
"license": "LGPL-3",
}
1 change: 1 addition & 0 deletions digital_signature_kmitl/models/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from . import res_users as res_users # noqa: F401
21 changes: 21 additions & 0 deletions digital_signature_kmitl/models/res_users.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
from odoo import fields, models


class ResUsers(models.Model):
_inherit = "res.users"

signature_image = fields.Image(
string="Digital Signature",
max_width=1024,
max_height=1024,
attachment=True,
help="Personal digital signature, captured into approval documents at approval time.",
)

@property
def SELF_READABLE_FIELDS(self):
return super().SELF_READABLE_FIELDS + ["signature_image"]

@property
def SELF_WRITEABLE_FIELDS(self):
return super().SELF_WRITEABLE_FIELDS + ["signature_image"]
38 changes: 38 additions & 0 deletions digital_signature_kmitl/views/res_users_views.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
<?xml version="1.0" encoding="utf-8" ?>
<odoo>
<record id="view_users_form_signature" model="ir.ui.view">
<field name="name">res.users.form.signature</field>
<field name="model">res.users</field>
<field name="inherit_id" ref="base.view_users_form" />
<field name="arch" type="xml">
<xpath expr="//page[@name='preferences']" position="inside">
<group string="ลายเซ็นดิจิตอล" name="digital_signature_image">
<field
name="signature_image"
widget="signature"
options="{'full_name': 'name'}"
nolabel="1"
/>
</group>
</xpath>
</field>
</record>

<record id="view_users_form_simple_modif_signature" model="ir.ui.view">
<field name="name">res.users.preferences.form.signature</field>
<field name="model">res.users</field>
<field name="inherit_id" ref="base.view_users_form_simple_modif" />
<field name="arch" type="xml">
<xpath expr="//page[@name='preferences_page']" position="inside">
<group string="ลายเซ็นดิจิตอล" name="digital_signature_image">
<field
name="signature_image"
widget="signature"
options="{'full_name': 'name'}"
nolabel="1"
/>
</group>
</xpath>
</field>
</record>
</odoo>
28 changes: 28 additions & 0 deletions digital_signature_kmitl/views/signature_templates.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<?xml version="1.0" encoding="utf-8" ?>
<odoo>
<template id="signature_block">
<div class="text-center">
<div style="white-space: nowrap;">
<span>(ลงชื่อ)</span>
<t t-if="signature">
<img
t-att-src="image_data_uri(signature)"
style="max-height: 2.2cm; max-width: 6cm; vertical-align: middle;"
/>
</t>
<t t-else="">
<span
class="sig-line"
style="display: inline-block; width: 200px; border-bottom: 1px dotted #000;"
/>
</t>
<t t-if="role">
<span t-out="role" />
</t>
</div>
<div class="mt-1">
(<span t-out="name or ''" />)
</div>
</div>
</template>
</odoo>
48 changes: 48 additions & 0 deletions purchase_work_acceptance_signature_kmitl/README.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
============================================
Purchase Work Acceptance Digital Signature
============================================

Automatic signature capture for Work Acceptance documents.

Features
========

* Snapshot procurement officer signature (``responsible_signature``) on document acceptance
* Snapshot committee member signatures (``committee.signature_image``) when they approve
* Immutable signature storage ensures audit trail and PDF consistency
* Automatic fallback to dotted signature lines for users without signatures
* Supports both paperless (tier validation) and paper-based approval workflows

Installation
============

Install in order:

#. ``digital_signature_kmitl``
#. ``purchase_work_acceptance_signature_kmitl``

Configuration
=============

No additional configuration required. Signatures are automatically captured from user profiles
when committee members or procurement officers approve the work acceptance.

Usage
=====

#. Ensure users have digital signatures set in their profiles
#. Create a work acceptance with committee members
#. Committee members approve via tier validation or wizard
#. Signatures are automatically captured into the document
#. Print PDF to see signature images (or dotted lines if signature not set)

PDF Reports
===========

* ``report_work_acceptance`` — Work Acceptance (พ.36) with committee signatures
* ``report_committee_acceptance`` — Committee Acceptance (ใบตรวจการรับพัสดุ) with committee signatures

Contributors
============

* Aginix Technologies
1 change: 1 addition & 0 deletions purchase_work_acceptance_signature_kmitl/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from . import models as models # noqa: F401
20 changes: 20 additions & 0 deletions purchase_work_acceptance_signature_kmitl/__manifest__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{
"name": "Purchase Work Acceptance Digital Signature",
"version": "16.0.1.0.0",
"summary": "Snapshot committee + procurement officer signatures into Work Acceptance "
"documents at approval time, and render them in the PDF report.",
"category": "KMITL",
"author": "Aginix Technologies",
"website": "https://github.com/aginix/kmitl",
"depends": [
"digital_signature_kmitl",
"purchase_work_acceptance_kmitl",
],
"data": [
"reports/report_work_acceptance.xml",
"reports/report_committee_acceptance.xml",
],
"installable": True,
"auto_install": False,
"license": "LGPL-3",
}
2 changes: 2 additions & 0 deletions purchase_work_acceptance_signature_kmitl/models/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
from . import work_acceptance as work_acceptance # noqa: F401
from . import work_acceptance_committee as work_acceptance_committee # noqa: F401
45 changes: 45 additions & 0 deletions purchase_work_acceptance_signature_kmitl/models/work_acceptance.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import logging

from odoo import api, fields, models

_logger = logging.getLogger(__name__)


class WorkAcceptance(models.Model):
_inherit = "work.acceptance"

responsible_signature = fields.Image(
string="Receiver Signature",
attachment=True,
copy=False,
readonly=True,
help="Signature of the procurement officer (responsible_id) captured at acceptance.",
)

def button_accept(self, force=False):
result = super().button_accept(force=force)
for rec in self:
if (
rec.state == "accept"
and not rec.responsible_signature
and rec.responsible_id
):
signature = rec.responsible_id.signature_image
if signature:
rec.with_context(skip_validation_check=True).write(
{"responsible_signature": signature}
)
else:
_logger.warning(
"User %s has no signature_image set, "
"responsible signature not captured for WA %s",
rec.responsible_id.name,
rec.name,
)
return result

@api.model
def _get_under_validation_exceptions(self):
res = super()._get_under_validation_exceptions()
res.append("responsible_signature")
return res
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import logging

from odoo import api, fields, models

_logger = logging.getLogger(__name__)


class WorkAcceptanceCommittee(models.Model):
_inherit = "work.acceptance.committee"

signature_image = fields.Image(
string="Committee Signature",
attachment=True,
copy=False,
readonly=True,
help="Signature of the committee member captured when the row is marked accepted.",
)

def write(self, vals):
result = super().write(vals)
if vals.get("status") == "accept":
for rec in self:
if not rec.signature_image:
user = rec.employee_id.user_id
if not user:
_logger.warning(
"Employee %s has no linked user, "
"signature not captured for committee record %s",
rec.employee_id.display_name,
rec.id,
)
elif user.signature_image:
super(WorkAcceptanceCommittee, rec).write(
{"signature_image": user.signature_image}
)
else:
_logger.warning(
"User %s has no signature_image set, "
"signature not captured for committee record %s",
user.name,
rec.id,
)
return result

@api.model
def _get_under_validation_exceptions(self):
res = super()._get_under_validation_exceptions()
res.append("signature_image")
return res
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
<?xml version="1.0" encoding="utf-8" ?>
<odoo>
<template
id="report_committee_acceptance_signature"
inherit_id="purchase_work_acceptance_kmitl.report_committee_acceptance"
>

<xpath
expr="//t[@t-foreach='o.work_acceptance_committee_ids'][1]"
position="replace"
>
<t t-foreach="o.work_acceptance_committee_ids" t-as="line">
<t t-if="line.approve_role != 'secretary'">
<div class="row mt-5">
<div class="col-9 offset-3 text-center">
<div style="white-space: nowrap;">
<span>(ลงชื่อ)</span>
<t t-if="line.signature_image">
<img
t-att-src="image_data_uri(line.signature_image)"
style="max-height: 2.2cm; max-width: 6cm; vertical-align: middle;"
/>
</t>
<t t-else="">
<span class="sig-line" />
</t>
<span t-field="line.approve_role" />
</div>
<div class="mt-2">
(<span t-esc="line.name" />)
</div>
</div>
</div>
</t>
</t>
</xpath>

<xpath
expr="//t[@t-foreach='o.work_acceptance_committee_ids'][2]"
position="replace"
>
<t t-foreach="o.work_acceptance_committee_ids" t-as="line">
<t t-if="line.approve_role == 'secretary'">
<div class="row mt-5">
<div class="col-9 offset-3 text-center">
<div style="white-space: nowrap;">
<span>(ลงชื่อ)</span>
<t t-if="line.signature_image">
<img
t-att-src="image_data_uri(line.signature_image)"
style="max-height: 2.2cm; max-width: 6cm; vertical-align: middle;"
/>
</t>
<t t-else="">
<span class="sig-line" />
</t>
<span t-field="line.approve_role" />
</div>
<div class="mt-2">
(<span t-esc="line.name" />)
</div>
</div>
</div>
</t>
</t>
</xpath>

</template>
</odoo>
Loading