diff --git a/digital_signature_kmitl/README.rst b/digital_signature_kmitl/README.rst new file mode 100644 index 000000000..6a9d55eb9 --- /dev/null +++ b/digital_signature_kmitl/README.rst @@ -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 diff --git a/digital_signature_kmitl/__init__.py b/digital_signature_kmitl/__init__.py new file mode 100644 index 000000000..6ab96431a --- /dev/null +++ b/digital_signature_kmitl/__init__.py @@ -0,0 +1 @@ +from . import models as models # noqa: F401 diff --git a/digital_signature_kmitl/__manifest__.py b/digital_signature_kmitl/__manifest__.py new file mode 100644 index 000000000..54d1bcbb2 --- /dev/null +++ b/digital_signature_kmitl/__manifest__.py @@ -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", +} diff --git a/digital_signature_kmitl/models/__init__.py b/digital_signature_kmitl/models/__init__.py new file mode 100644 index 000000000..35f4e21de --- /dev/null +++ b/digital_signature_kmitl/models/__init__.py @@ -0,0 +1 @@ +from . import res_users as res_users # noqa: F401 diff --git a/digital_signature_kmitl/models/res_users.py b/digital_signature_kmitl/models/res_users.py new file mode 100644 index 000000000..1ccff7c62 --- /dev/null +++ b/digital_signature_kmitl/models/res_users.py @@ -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"] diff --git a/digital_signature_kmitl/views/res_users_views.xml b/digital_signature_kmitl/views/res_users_views.xml new file mode 100644 index 000000000..b973142db --- /dev/null +++ b/digital_signature_kmitl/views/res_users_views.xml @@ -0,0 +1,38 @@ + + + + res.users.form.signature + res.users + + + + + + + + + + + + res.users.preferences.form.signature + res.users + + + + + + + + + + diff --git a/digital_signature_kmitl/views/signature_templates.xml b/digital_signature_kmitl/views/signature_templates.xml new file mode 100644 index 000000000..d0ce8565b --- /dev/null +++ b/digital_signature_kmitl/views/signature_templates.xml @@ -0,0 +1,28 @@ + + + + diff --git a/purchase_work_acceptance_signature_kmitl/README.rst b/purchase_work_acceptance_signature_kmitl/README.rst new file mode 100644 index 000000000..43c08f2a0 --- /dev/null +++ b/purchase_work_acceptance_signature_kmitl/README.rst @@ -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 diff --git a/purchase_work_acceptance_signature_kmitl/__init__.py b/purchase_work_acceptance_signature_kmitl/__init__.py new file mode 100644 index 000000000..6ab96431a --- /dev/null +++ b/purchase_work_acceptance_signature_kmitl/__init__.py @@ -0,0 +1 @@ +from . import models as models # noqa: F401 diff --git a/purchase_work_acceptance_signature_kmitl/__manifest__.py b/purchase_work_acceptance_signature_kmitl/__manifest__.py new file mode 100644 index 000000000..e89273d3c --- /dev/null +++ b/purchase_work_acceptance_signature_kmitl/__manifest__.py @@ -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", +} diff --git a/purchase_work_acceptance_signature_kmitl/models/__init__.py b/purchase_work_acceptance_signature_kmitl/models/__init__.py new file mode 100644 index 000000000..5e95b9be9 --- /dev/null +++ b/purchase_work_acceptance_signature_kmitl/models/__init__.py @@ -0,0 +1,2 @@ +from . import work_acceptance as work_acceptance # noqa: F401 +from . import work_acceptance_committee as work_acceptance_committee # noqa: F401 diff --git a/purchase_work_acceptance_signature_kmitl/models/work_acceptance.py b/purchase_work_acceptance_signature_kmitl/models/work_acceptance.py new file mode 100644 index 000000000..670b830a7 --- /dev/null +++ b/purchase_work_acceptance_signature_kmitl/models/work_acceptance.py @@ -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 diff --git a/purchase_work_acceptance_signature_kmitl/models/work_acceptance_committee.py b/purchase_work_acceptance_signature_kmitl/models/work_acceptance_committee.py new file mode 100644 index 000000000..c1d83f482 --- /dev/null +++ b/purchase_work_acceptance_signature_kmitl/models/work_acceptance_committee.py @@ -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 diff --git a/purchase_work_acceptance_signature_kmitl/reports/report_committee_acceptance.xml b/purchase_work_acceptance_signature_kmitl/reports/report_committee_acceptance.xml new file mode 100644 index 000000000..d99e8539e --- /dev/null +++ b/purchase_work_acceptance_signature_kmitl/reports/report_committee_acceptance.xml @@ -0,0 +1,69 @@ + + + + diff --git a/purchase_work_acceptance_signature_kmitl/reports/report_work_acceptance.xml b/purchase_work_acceptance_signature_kmitl/reports/report_work_acceptance.xml new file mode 100644 index 000000000..b076d3202 --- /dev/null +++ b/purchase_work_acceptance_signature_kmitl/reports/report_work_acceptance.xml @@ -0,0 +1,90 @@ + + + +