feat: Add sequence suffix to sign items, improve popover handling, and refine backend saving for new items.
This commit is contained in:
parent
7092737ec4
commit
1074212728
27
README.md
Normal file
27
README.md
Normal file
@ -0,0 +1,27 @@
|
||||
# Sign Sequence Field
|
||||
|
||||
Add a Sequence field type to Odoo Sign templates to automatically generate numbers upon document completion.
|
||||
|
||||
## Features
|
||||
|
||||
- **New Field Type**: Adds a "Sequence" field to the Sign template editor toolbar.
|
||||
- **Auto-Generation**: Automatically generates the next sequence number from a linked `ir.sequence` when all signers have completed the document.
|
||||
- **Live Editing**: Edit Sequence Prefix, Suffix, Padding, and Next Number directly from the template editor sidebar.
|
||||
- **Dynamic Positioning**: Place the sequence number anywhere on your document just like any other sign field.
|
||||
|
||||
## Usage
|
||||
|
||||
1. **Edit a Template**: Go to Sign -> Templates and open a template for editing.
|
||||
2. **Drag & Drop**: Drag the "Sequence" field from the left toolbar onto your document.
|
||||
3. **Configure**: Click on the sequence field to open its configuration popover:
|
||||
- Select an existing **Sequence** record.
|
||||
- (Optional) Modify the **Prefix**, **Suffix**, **Padding**, or **Next Number**.
|
||||
4. **Save**: Save the template.
|
||||
5. **Sign**: When a document created from this template is fully signed, the sequence number will be generated and stamped onto the final PDF.
|
||||
|
||||
## Technical Details
|
||||
|
||||
- **Module Name**: `sign_sequence_field`
|
||||
- **Dependency**: `sign` (Enterprise)
|
||||
- **Odoo Version**: 19.0
|
||||
- **Authors**: Suherdy Yacob
|
||||
@ -1,5 +1,6 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import io
|
||||
import logging
|
||||
from odoo import models
|
||||
from odoo.exceptions import ValidationError
|
||||
from odoo.tools.pdf import PdfFileReader, PdfFileWriter, PdfReadError, reshape_text
|
||||
@ -10,6 +11,8 @@ try:
|
||||
except ImportError:
|
||||
stringWidth = None
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
class SignDocument(models.Model):
|
||||
_inherit = 'sign.document'
|
||||
|
||||
@ -26,7 +29,9 @@ class SignDocument(models.Model):
|
||||
for item in page_items
|
||||
)
|
||||
|
||||
_logger.info("SignSequenceField: render_document_with_items started")
|
||||
if not has_sequence_items:
|
||||
_logger.info("SignSequenceField: No sequence items found in document, skipping custom render")
|
||||
return base_output
|
||||
|
||||
try:
|
||||
@ -91,7 +96,9 @@ class SignDocument(models.Model):
|
||||
can.showPage()
|
||||
|
||||
can.save()
|
||||
_logger.info("SignSequenceField: Custom canvas saved to packet")
|
||||
|
||||
packet.seek(0)
|
||||
item_pdf = PdfFileReader(packet)
|
||||
new_pdf = PdfFileWriter()
|
||||
|
||||
@ -105,6 +112,9 @@ class SignDocument(models.Model):
|
||||
try:
|
||||
new_pdf.write(output)
|
||||
except PdfReadError:
|
||||
_logger.error("SignSequenceField: PdfReadError during final PDF write")
|
||||
raise ValidationError(self.env._("There was an issue generating the document."))
|
||||
|
||||
_logger.info("SignSequenceField: Custom rendering completed successfully")
|
||||
output.seek(0)
|
||||
return output
|
||||
|
||||
@ -43,18 +43,26 @@ class SignRequest(models.Model):
|
||||
# We only generate if we are about to complete the signing process.
|
||||
# The logic in _post_fill_request_item calls _sign only when all items are completed.
|
||||
|
||||
_logger.info("SignSequenceField: Starting sequence generation for sign.request %s", self.ids)
|
||||
for request in self:
|
||||
# Generate sequence numbers for sequence items
|
||||
for sign_item in request.template_id.sign_item_ids:
|
||||
# Use template_id.document_ids.sign_item_ids to be safe
|
||||
signer_items = request.template_id.document_ids.sign_item_ids
|
||||
_logger.info("SignSequenceField: Found %s total sign items on template documents for request %s", len(signer_items), request.id)
|
||||
for sign_item in signer_items:
|
||||
_logger.debug("SignSequenceField: Checking item %s: type=%s, sequence_id=%s", sign_item.id, sign_item.type_id.item_type, sign_item.sequence_id.id if sign_item.sequence_id else 'None')
|
||||
if sign_item.type_id.item_type == 'sequence' and sign_item.sequence_id:
|
||||
_logger.info("SignSequenceField: Processing sequence item %s (ID: %s) for request %s", sign_item.name, sign_item.id, request.id)
|
||||
# Find the responsible request item (signer)
|
||||
# Use role_id to match.
|
||||
# Fallback to the first signer if no specific matching signer is found (e.g. role is "Anyone")
|
||||
request_items = request.request_item_ids.filtered(lambda r: r.role_id == sign_item.responsible_id)
|
||||
if not request_items:
|
||||
_logger.info("SignSequenceField: No matching request items for role %s, falling back to first signer", sign_item.responsible_id.name)
|
||||
request_items = request.request_item_ids[:1]
|
||||
|
||||
if not request_items:
|
||||
_logger.warning("SignSequenceField: No request items found at all for request %s", request.id)
|
||||
continue
|
||||
|
||||
# We only generate if it hasn't been generated yet for this sign_item across the whole request
|
||||
@ -64,16 +72,24 @@ class SignRequest(models.Model):
|
||||
('sign_item_id', '=', sign_item.id)
|
||||
], limit=1)
|
||||
|
||||
# If it exists but value is empty/false OR it equals the placeholder "Sequence Number"
|
||||
placeholder = (sign_item.type_id.placeholder or "Sequence Number").strip().lower()
|
||||
current_val = str(existing_value.value or "").strip().lower()
|
||||
_logger.debug("SignSequenceField: existing_value check for item %s: '%s' (placeholder: '%s')", sign_item.id, current_val, placeholder)
|
||||
|
||||
# If it exists but value is empty/false OR it equals the placeholder
|
||||
# we must regenerate it.
|
||||
if not existing_value or not existing_value.value or existing_value.value == "Sequence Number":
|
||||
if not existing_value or not current_val or current_val == placeholder:
|
||||
_logger.info("SignSequenceField: Generating sequence for item %s", sign_item.id)
|
||||
new_seq = sign_item.sequence_id.next_by_id()
|
||||
_logger.info("SignSequenceField: Generated new sequence: %s", new_seq)
|
||||
if new_seq:
|
||||
if existing_value:
|
||||
# Update existing empty record
|
||||
_logger.debug("SignSequenceField: Updating existing value record %s", existing_value.id)
|
||||
existing_value.write({'value': new_seq})
|
||||
else:
|
||||
# Create value for the first matching signer
|
||||
_logger.debug("SignSequenceField: Creating new value record for signer %s", request_items[0].id)
|
||||
request.env['sign.request.item.value'].create({
|
||||
'sign_request_item_id': request_items[0].id,
|
||||
'sign_item_id': sign_item.id,
|
||||
@ -85,7 +101,12 @@ class SignRequest(models.Model):
|
||||
# request.reference is the document name.
|
||||
# Check if already renamed to avoid double prefixing
|
||||
if not request.reference.startswith(f"{new_seq} - "):
|
||||
_logger.info("SignSequenceField: Prefixing document name with sequence: %s", new_seq)
|
||||
request.write({'reference': f"{new_seq} - {request.reference}"})
|
||||
else:
|
||||
_logger.error("SignSequenceField: next_by_id() returned None for sequence %s", sign_item.sequence_id.id)
|
||||
else:
|
||||
_logger.info("SignSequenceField: Sequence already exists for item %s: %s", sign_item.id, existing_value.value)
|
||||
|
||||
return super()._sign()
|
||||
|
||||
|
||||
@ -8,6 +8,10 @@ import { patch } from "@web/core/utils/patch";
|
||||
|
||||
patch(SignTemplateIframe.prototype, {
|
||||
async openSignItemPopup(signItem) {
|
||||
// Save the item to the backend if it has a negative ID, as it indicates the item is new and not yet linked to the template.
|
||||
if (signItem.data.id < 0) {
|
||||
await this.saveChangesOnBackend();
|
||||
}
|
||||
const shouldOpenNewPopover = !(signItem.data.id in this.closePopoverFns);
|
||||
this.closePopover();
|
||||
if (shouldOpenNewPopover) {
|
||||
@ -20,20 +24,20 @@ patch(SignTemplateIframe.prototype, {
|
||||
SignItemCustomPopover,
|
||||
{
|
||||
debug: this.env.debug,
|
||||
responsible: signItem.data.responsible,
|
||||
roles: this.signRolesById,
|
||||
alignment: signItem.data.alignment,
|
||||
required: signItem.data.required,
|
||||
constant: signItem.data.constant,
|
||||
header_title: header_title,
|
||||
placeholder: signItem.data.placeholder,
|
||||
id: signItem.data.id,
|
||||
type: signItem.data.type,
|
||||
option_ids: signItem.data.option_ids,
|
||||
num_options: this.getSignItemById(signItem.data.id).data.num_options,
|
||||
num_options: this.getSignItemById(signItem.data.id)?.data.num_options,
|
||||
radio_set_id: signItem.data.radio_set_id,
|
||||
// PATCH START
|
||||
sequence_id: signItem.data.sequence_id,
|
||||
sequence_prefix: signItem.data.sequence_prefix,
|
||||
sequence_suffix: signItem.data.sequence_suffix,
|
||||
sequence_padding: signItem.data.sequence_padding,
|
||||
sequence_number_next: signItem.data.sequence_number_next,
|
||||
// PATCH END
|
||||
@ -45,18 +49,26 @@ patch(SignTemplateIframe.prototype, {
|
||||
this.closePopover();
|
||||
this.deleteSignItem(signItem);
|
||||
},
|
||||
onDuplicate: () => {
|
||||
this.closePopover();
|
||||
this.duplicateSignItem(signItem);
|
||||
},
|
||||
onClose: () => {
|
||||
this.closePopover();
|
||||
},
|
||||
updateSelectionOptions: (ids) => this.updateSelectionOptions(ids),
|
||||
updateRoles: (id) => this.updateRoles(id),
|
||||
onCopyItem: (id) => this.onCopyItem(id),
|
||||
},
|
||||
{
|
||||
position: "right",
|
||||
onClose: () => {
|
||||
this.closePopoverFns = {};
|
||||
},
|
||||
closeOnClickAway: (target) => !target.closest(".modal"),
|
||||
closeOnClickAway: (target) => {
|
||||
if (!target.closest(".popover")) {
|
||||
this.closePopover();
|
||||
}
|
||||
return !target.closest(".popover");
|
||||
},
|
||||
popoverClass: "sign-popover",
|
||||
}
|
||||
);
|
||||
@ -69,7 +81,7 @@ patch(SignTemplateIframe.prototype, {
|
||||
|
||||
updateSignItem(signItem, data) {
|
||||
// Ensure sequence fields are not filtered out if they were missing from initial load
|
||||
const sequenceFields = ['sequence_id', 'sequence_prefix', 'sequence_padding', 'sequence_number_next'];
|
||||
const sequenceFields = ['sequence_id', 'sequence_prefix', 'sequence_suffix', 'sequence_padding', 'sequence_number_next'];
|
||||
for (const field of sequenceFields) {
|
||||
if (field in data && !(field in signItem.data)) {
|
||||
signItem.data[field] = false; // Initialize with dummy value to pass the check
|
||||
@ -82,21 +94,24 @@ patch(SignTemplateIframe.prototype, {
|
||||
patch(SignTemplateBody.prototype, {
|
||||
prepareTemplateData() {
|
||||
const [updatedSignItems, Id2UpdatedItem] = super.prepareTemplateData();
|
||||
const items = this.iframe?.signItems ?? {};
|
||||
const items = this.props.iframe?.signItems ?? {};
|
||||
|
||||
for (const page in items) {
|
||||
for (const id in items[page]) {
|
||||
const signItem = items[page][id].data;
|
||||
// updatedSignItems is keyed by ID. Check if this item is in the updated list.
|
||||
if (updatedSignItems[id]) {
|
||||
if (signItem.sequence_id) {
|
||||
// specific handling for Many2one: take ID if it's an array
|
||||
updatedSignItems[id].sequence_id = Array.isArray(signItem.sequence_id) ? signItem.sequence_id[0] : signItem.sequence_id;
|
||||
if (signItem.type === 'sequence') {
|
||||
if (signItem.sequence_id) {
|
||||
// specific handling for Many2one: take ID if it's an array
|
||||
updatedSignItems[id].sequence_id = Array.isArray(signItem.sequence_id) ? signItem.sequence_id[0] : signItem.sequence_id;
|
||||
}
|
||||
// For related fields, we want to save them if they changed.
|
||||
if (signItem.sequence_prefix !== undefined) updatedSignItems[id].sequence_prefix = signItem.sequence_prefix;
|
||||
if (signItem.sequence_suffix !== undefined) updatedSignItems[id].sequence_suffix = signItem.sequence_suffix;
|
||||
if (signItem.sequence_padding !== undefined) updatedSignItems[id].sequence_padding = signItem.sequence_padding;
|
||||
if (signItem.sequence_number_next !== undefined) updatedSignItems[id].sequence_number_next = signItem.sequence_number_next;
|
||||
}
|
||||
// For related fields, we want to save them if they changed.
|
||||
if (signItem.sequence_prefix !== undefined) updatedSignItems[id].sequence_prefix = signItem.sequence_prefix;
|
||||
if (signItem.sequence_padding !== undefined) updatedSignItems[id].sequence_padding = signItem.sequence_padding;
|
||||
if (signItem.sequence_number_next !== undefined) updatedSignItems[id].sequence_number_next = signItem.sequence_number_next;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -9,6 +9,7 @@ patch(SignItemCustomPopover.prototype, {
|
||||
super.setup();
|
||||
this.state.sequence_id = this.props.sequence_id || false;
|
||||
this.state.sequence_prefix = this.props.sequence_prefix || "";
|
||||
this.state.sequence_suffix = this.props.sequence_suffix || "";
|
||||
this.state.sequence_padding = this.props.sequence_padding || 0;
|
||||
this.state.sequence_number_next = this.props.sequence_number_next || 1;
|
||||
|
||||
@ -17,7 +18,8 @@ patch(SignItemCustomPopover.prototype, {
|
||||
|
||||
if (this.props.sequence_id) {
|
||||
this.state.sequence_value = "Loading...";
|
||||
this.orm.read("ir.sequence", [this.props.sequence_id], ["display_name"]).then((res) => {
|
||||
const sequenceId = Array.isArray(this.props.sequence_id) ? this.props.sequence_id[0] : this.props.sequence_id;
|
||||
this.orm.read("ir.sequence", [sequenceId], ["display_name"]).then((res) => {
|
||||
if (res && res.length) {
|
||||
this.state.sequence_value = res[0].display_name;
|
||||
} else {
|
||||
@ -36,14 +38,16 @@ patch(SignItemCustomPopover.prototype, {
|
||||
this.state.sequence_id = sequenceId;
|
||||
|
||||
if (sequenceId) {
|
||||
const result = await this.orm.read("ir.sequence", [sequenceId], ["prefix", "padding", "number_next"]);
|
||||
const result = await this.orm.read("ir.sequence", [sequenceId], ["prefix", "suffix", "padding", "number_next"]);
|
||||
if (result && result.length) {
|
||||
this.state.sequence_prefix = result[0].prefix || "";
|
||||
this.state.sequence_suffix = result[0].suffix || "";
|
||||
this.state.sequence_padding = result[0].padding || 0;
|
||||
this.state.sequence_number_next = result[0].number_next || 1;
|
||||
}
|
||||
} else {
|
||||
this.state.sequence_prefix = "";
|
||||
this.state.sequence_suffix = "";
|
||||
this.state.sequence_padding = 0;
|
||||
this.state.sequence_number_next = 1;
|
||||
}
|
||||
|
||||
@ -24,6 +24,10 @@
|
||||
<label class="form-label">Prefix</label>
|
||||
<input type="text" class="form-control" t-att-value="state.sequence_prefix" t-on-change="(e) => this.onChange('sequence_prefix', e.target.value)" />
|
||||
</div>
|
||||
<div class="mb-2">
|
||||
<label class="form-label">Suffix</label>
|
||||
<input type="text" class="form-control" t-att-value="state.sequence_suffix" t-on-change="(e) => this.onChange('sequence_suffix', e.target.value)" />
|
||||
</div>
|
||||
<div class="mb-2">
|
||||
<label class="form-label">Next Number</label>
|
||||
<input type="number" class="form-control" t-att-value="state.sequence_number_next" t-on-change="(e) => this.onChange('sequence_number_next', parseInt(e.target.value))" />
|
||||
|
||||
Loading…
Reference in New Issue
Block a user