first commit
This commit is contained in:
commit
923ac57db6
2
__init__.py
Normal file
2
__init__.py
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from . import models
|
||||||
35
__manifest__.py
Normal file
35
__manifest__.py
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
{
|
||||||
|
'name': "Sign Sequence Field",
|
||||||
|
'summary': "Add a Sequence field type to Sign templates to auto-generate numbers",
|
||||||
|
'description': """
|
||||||
|
This module extends the Odoo Sign app to add a new field type 'Sequence'.
|
||||||
|
When dropped onto a template, it allows linking to an ir.sequence.
|
||||||
|
Upon document completion, the sequence number is generated and stamped on the document.
|
||||||
|
User can edit the sequence format directly from the field properties.
|
||||||
|
""",
|
||||||
|
'author': "Mapan",
|
||||||
|
'category': 'Sign',
|
||||||
|
'version': '0.1',
|
||||||
|
'depends': ['sign'],
|
||||||
|
'data': [
|
||||||
|
'data/sign_item_type_data.xml',
|
||||||
|
'views/sign_item_views.xml',
|
||||||
|
],
|
||||||
|
'assets': {
|
||||||
|
'web.assets_frontend': [
|
||||||
|
'sign_sequence_field/static/src/xml/sign_item_sequence.xml',
|
||||||
|
],
|
||||||
|
'web.assets_backend': [
|
||||||
|
'sign_sequence_field/static/src/xml/sign_item_sequence.xml',
|
||||||
|
'sign_sequence_field/static/src/js/sign_item_custom_popover_patch.js',
|
||||||
|
'sign_sequence_field/static/src/js/sign_backend_patch.js',
|
||||||
|
'sign_sequence_field/static/src/xml/sign_item_custom_popover_patch.xml',
|
||||||
|
],
|
||||||
|
'sign.assets_public_sign': [
|
||||||
|
'sign_sequence_field/static/src/xml/sign_item_sequence.xml',
|
||||||
|
]
|
||||||
|
},
|
||||||
|
'demo': [],
|
||||||
|
'license': 'LGPL-3',
|
||||||
|
}
|
||||||
BIN
__pycache__/__init__.cpython-312.pyc
Normal file
BIN
__pycache__/__init__.cpython-312.pyc
Normal file
Binary file not shown.
12
data/sign_item_type_data.xml
Normal file
12
data/sign_item_type_data.xml
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<odoo>
|
||||||
|
<record id="sign_item_type_sequence" model="sign.item.type">
|
||||||
|
<field name="name">Sequence</field>
|
||||||
|
<field name="item_type">sequence</field>
|
||||||
|
<field name="icon">fa-sort-numeric-asc</field>
|
||||||
|
<field name="tip">Auto-generated sequence number</field>
|
||||||
|
<field name="placeholder">Sequence Number</field>
|
||||||
|
<field name="default_width">0.2</field>
|
||||||
|
<field name="default_height">0.05</field>
|
||||||
|
</record>
|
||||||
|
</odoo>
|
||||||
4
models/__init__.py
Normal file
4
models/__init__.py
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from . import sign_item_type
|
||||||
|
from . import sign_item
|
||||||
|
from . import sign_request
|
||||||
BIN
models/__pycache__/__init__.cpython-312.pyc
Normal file
BIN
models/__pycache__/__init__.cpython-312.pyc
Normal file
Binary file not shown.
BIN
models/__pycache__/sign_item.cpython-312.pyc
Normal file
BIN
models/__pycache__/sign_item.cpython-312.pyc
Normal file
Binary file not shown.
BIN
models/__pycache__/sign_item_type.cpython-312.pyc
Normal file
BIN
models/__pycache__/sign_item_type.cpython-312.pyc
Normal file
Binary file not shown.
BIN
models/__pycache__/sign_request.cpython-312.pyc
Normal file
BIN
models/__pycache__/sign_request.cpython-312.pyc
Normal file
Binary file not shown.
33
models/sign_item.py
Normal file
33
models/sign_item.py
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from odoo import api, fields, models, _
|
||||||
|
from odoo.exceptions import UserError
|
||||||
|
|
||||||
|
class SignItem(models.Model):
|
||||||
|
_inherit = "sign.item"
|
||||||
|
|
||||||
|
sequence_id = fields.Many2one('ir.sequence', string="Sequence")
|
||||||
|
item_type = fields.Selection(related='type_id.item_type')
|
||||||
|
|
||||||
|
# Proxy fields to allow editing sequence from the sign item
|
||||||
|
sequence_prefix = fields.Char(related='sequence_id.prefix', readonly=False)
|
||||||
|
sequence_padding = fields.Integer(related='sequence_id.padding', readonly=False)
|
||||||
|
sequence_number_next = fields.Integer(related='sequence_id.number_next_actual', readonly=False, string="Next Number")
|
||||||
|
|
||||||
|
@api.onchange('type_id')
|
||||||
|
def _onchange_type_id_sequence(self):
|
||||||
|
if self.type_id.item_type == 'sequence' and not self.sequence_id:
|
||||||
|
# Optional: auto-create or encourage selection?
|
||||||
|
# For now, just leave empty for user to select.
|
||||||
|
pass
|
||||||
|
|
||||||
|
def action_create_sequence(self):
|
||||||
|
""" Helper to create a new sequence for this item if needed """
|
||||||
|
self.ensure_one()
|
||||||
|
if not self.sequence_id:
|
||||||
|
seq = self.env['ir.sequence'].create({
|
||||||
|
'name': f"Sign Sequence - {self.template_id.name}",
|
||||||
|
'code': f"sign.item.{self.id}",
|
||||||
|
'prefix': 'SEQ-',
|
||||||
|
'padding': 4,
|
||||||
|
})
|
||||||
|
self.sequence_id = seq
|
||||||
7
models/sign_item_type.py
Normal file
7
models/sign_item_type.py
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from odoo import fields, models
|
||||||
|
|
||||||
|
class SignItemType(models.Model):
|
||||||
|
_inherit = "sign.item.type"
|
||||||
|
|
||||||
|
item_type = fields.Selection(selection_add=[('sequence', "Sequence")], ondelete={'sequence': 'cascade'})
|
||||||
274
models/sign_request.py
Normal file
274
models/sign_request.py
Normal file
@ -0,0 +1,274 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
import base64
|
||||||
|
import io
|
||||||
|
import os
|
||||||
|
import time
|
||||||
|
from dateutil.relativedelta import relativedelta
|
||||||
|
from datetime import timedelta
|
||||||
|
|
||||||
|
from reportlab.lib.utils import ImageReader
|
||||||
|
from reportlab.pdfbase import pdfmetrics
|
||||||
|
from reportlab.pdfbase.ttfonts import TTFont
|
||||||
|
from reportlab.rl_config import TTFSearchPath
|
||||||
|
from reportlab.pdfgen import canvas
|
||||||
|
from reportlab.platypus import Paragraph
|
||||||
|
from reportlab.lib.styles import ParagraphStyle
|
||||||
|
from reportlab.pdfbase.pdfmetrics import stringWidth
|
||||||
|
from PIL import UnidentifiedImageError
|
||||||
|
|
||||||
|
from odoo import api, fields, models, _, Command
|
||||||
|
from odoo.exceptions import UserError, ValidationError
|
||||||
|
from odoo.tools.pdf import PdfFileReader, PdfFileWriter, PdfReadError, reshape_text
|
||||||
|
|
||||||
|
# Helper function copied from sign/models/sign_request.py
|
||||||
|
def _fix_image_transparency(image):
|
||||||
|
pixels = image.load()
|
||||||
|
for x in range(image.size[0]):
|
||||||
|
for y in range(image.size[1]):
|
||||||
|
if pixels[x, y] == (0, 0, 0, 0):
|
||||||
|
pixels[x, y] = (255, 255, 255, 0)
|
||||||
|
|
||||||
|
class SignRequest(models.Model):
|
||||||
|
_inherit = "sign.request"
|
||||||
|
|
||||||
|
def _sign(self):
|
||||||
|
""" Override to generate sequence numbers when request is signed """
|
||||||
|
# We perform sequence generation BEFORE calling super()._sign()
|
||||||
|
# because super()._sign() triggers _send_completed_document() immediately.
|
||||||
|
# We need the sequence values to be ready before the document is generated and sent.
|
||||||
|
|
||||||
|
# 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.
|
||||||
|
|
||||||
|
for request in self:
|
||||||
|
# Generate sequence numbers for sequence items
|
||||||
|
for sign_item in request.template_id.sign_item_ids:
|
||||||
|
if sign_item.type_id.item_type == 'sequence' and sign_item.sequence_id:
|
||||||
|
# Find the responsible request item (signer)
|
||||||
|
# Use role_id to match.
|
||||||
|
# We check if there is a signer for this role in this request.
|
||||||
|
request_items = request.request_item_ids.filtered(lambda r: r.role_id == sign_item.responsible_id)
|
||||||
|
if not request_items:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# We only generate if it hasn't been generated yet.
|
||||||
|
existing_value = request.env['sign.request.item.value'].search([
|
||||||
|
('sign_request_item_id', 'in', request_items.ids),
|
||||||
|
('sign_item_id', '=', sign_item.id)
|
||||||
|
], limit=1)
|
||||||
|
|
||||||
|
# If it exists but value is empty/false OR it equals the placeholder "Sequence Number"
|
||||||
|
# we must regenerate it.
|
||||||
|
if not existing_value or not existing_value.value or existing_value.value == "Sequence Number":
|
||||||
|
new_seq = sign_item.sequence_id.next_by_id()
|
||||||
|
if new_seq:
|
||||||
|
if existing_value:
|
||||||
|
# Update existing empty record
|
||||||
|
existing_value.write({'value': new_seq})
|
||||||
|
else:
|
||||||
|
# Create value for the first matching signer
|
||||||
|
request.env['sign.request.item.value'].create({
|
||||||
|
'sign_request_item_id': request_items[0].id,
|
||||||
|
'sign_item_id': sign_item.id,
|
||||||
|
'value': new_seq
|
||||||
|
})
|
||||||
|
|
||||||
|
# Rename the document if requested
|
||||||
|
# User asked to put sequence in prefix of document name.
|
||||||
|
# request.reference is the document name.
|
||||||
|
# Check if already renamed to avoid double prefixing
|
||||||
|
if not request.reference.startswith(f"{new_seq} - "):
|
||||||
|
request.write({'reference': f"{new_seq} - {request.reference}"})
|
||||||
|
|
||||||
|
return super(SignRequest, self)._sign()
|
||||||
|
|
||||||
|
def _generate_completed_document(self, password=""):
|
||||||
|
# We need to override this entire method to handle 'sequence' item type
|
||||||
|
# as the original method doesn't have hooks or generic handling.
|
||||||
|
self.ensure_one()
|
||||||
|
if self.state != 'signed':
|
||||||
|
raise UserError(_("The completed document cannot be created because the sign request is not fully signed"))
|
||||||
|
if not self.template_id.sign_item_ids:
|
||||||
|
self.completed_document = self.template_id.attachment_id.datas
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
old_pdf = PdfFileReader(io.BytesIO(base64.b64decode(self.template_id.attachment_id.datas)), strict=False, overwriteWarnings=False)
|
||||||
|
old_pdf.getNumPages()
|
||||||
|
except:
|
||||||
|
raise ValidationError(_("ERROR: Invalid PDF file!"))
|
||||||
|
|
||||||
|
isEncrypted = old_pdf.isEncrypted
|
||||||
|
if isEncrypted and not old_pdf.decrypt(password):
|
||||||
|
# password is not correct
|
||||||
|
return
|
||||||
|
|
||||||
|
font = self._get_font()
|
||||||
|
normalFontSize = self._get_normal_font_size()
|
||||||
|
|
||||||
|
packet = io.BytesIO()
|
||||||
|
can = canvas.Canvas(packet, pagesize=self.get_page_size(old_pdf))
|
||||||
|
itemsByPage = self.template_id._get_sign_items_by_page()
|
||||||
|
items_ids = [id for items in itemsByPage.values() for id in items.ids]
|
||||||
|
values_dict = self.env['sign.request.item.value']._read_group(
|
||||||
|
[('sign_item_id', 'in', items_ids), ('sign_request_id', '=', self.id)],
|
||||||
|
groupby=['sign_item_id'],
|
||||||
|
aggregates=['value:array_agg', 'frame_value:array_agg', 'frame_has_hash:array_agg']
|
||||||
|
)
|
||||||
|
values = {
|
||||||
|
sign_item.id : {
|
||||||
|
'value': values[0],
|
||||||
|
'frame': frame_values[0],
|
||||||
|
'frame_has_hash': frame_has_hashes[0],
|
||||||
|
}
|
||||||
|
for sign_item, values, frame_values, frame_has_hashes in values_dict
|
||||||
|
}
|
||||||
|
|
||||||
|
for p in range(0, old_pdf.getNumPages()):
|
||||||
|
page = old_pdf.getPage(p)
|
||||||
|
# Absolute values are taken as it depends on the MediaBox template PDF metadata, they may be negative
|
||||||
|
width = float(abs(page.mediaBox.getWidth()))
|
||||||
|
height = float(abs(page.mediaBox.getHeight()))
|
||||||
|
|
||||||
|
# Set page orientation (either 0, 90, 180 or 270)
|
||||||
|
rotation = page['/Rotate'] if '/Rotate' in page else 0
|
||||||
|
if rotation and isinstance(rotation, int):
|
||||||
|
can.rotate(rotation)
|
||||||
|
# Translate system so that elements are placed correctly
|
||||||
|
# despite of the orientation
|
||||||
|
if rotation == 90:
|
||||||
|
width, height = height, width
|
||||||
|
can.translate(0, -height)
|
||||||
|
elif rotation == 180:
|
||||||
|
can.translate(-width, -height)
|
||||||
|
elif rotation == 270:
|
||||||
|
width, height = height, width
|
||||||
|
can.translate(-width, 0)
|
||||||
|
|
||||||
|
items = itemsByPage[p + 1] if p + 1 in itemsByPage else []
|
||||||
|
for item in items:
|
||||||
|
value_dict = values.get(item.id)
|
||||||
|
if not value_dict:
|
||||||
|
continue
|
||||||
|
# only get the 1st
|
||||||
|
value = value_dict['value']
|
||||||
|
frame = value_dict['frame']
|
||||||
|
|
||||||
|
if frame:
|
||||||
|
try:
|
||||||
|
image_reader = ImageReader(io.BytesIO(base64.b64decode(frame[frame.find(',')+1:])))
|
||||||
|
except UnidentifiedImageError:
|
||||||
|
raise ValidationError(_("There was an issue downloading your document. Please contact an administrator."))
|
||||||
|
_fix_image_transparency(image_reader._image)
|
||||||
|
can.drawImage(
|
||||||
|
image_reader,
|
||||||
|
width*item.posX,
|
||||||
|
height*(1-item.posY-item.height),
|
||||||
|
width*item.width,
|
||||||
|
height*item.height,
|
||||||
|
'auto',
|
||||||
|
True
|
||||||
|
)
|
||||||
|
|
||||||
|
# LOGIC FOR SEQUENCE TYPE (Same as text)
|
||||||
|
if item.type_id.item_type == "sequence":
|
||||||
|
if value:
|
||||||
|
value = reshape_text(value)
|
||||||
|
can.setFont(font, height*item.height*0.8)
|
||||||
|
if item.alignment == "left":
|
||||||
|
can.drawString(width*item.posX, height*(1-item.posY-item.height*0.9), value)
|
||||||
|
elif item.alignment == "right":
|
||||||
|
can.drawRightString(width*(item.posX+item.width), height*(1-item.posY-item.height*0.9), value)
|
||||||
|
else:
|
||||||
|
can.drawCentredString(width*(item.posX+item.width/2), height*(1-item.posY-item.height*0.9), value)
|
||||||
|
|
||||||
|
elif item.type_id.item_type == "text":
|
||||||
|
value = reshape_text(value)
|
||||||
|
can.setFont(font, height*item.height*0.8)
|
||||||
|
if item.alignment == "left":
|
||||||
|
can.drawString(width*item.posX, height*(1-item.posY-item.height*0.9), value)
|
||||||
|
elif item.alignment == "right":
|
||||||
|
can.drawRightString(width*(item.posX+item.width), height*(1-item.posY-item.height*0.9), value)
|
||||||
|
else:
|
||||||
|
can.drawCentredString(width*(item.posX+item.width/2), height*(1-item.posY-item.height*0.9), value)
|
||||||
|
|
||||||
|
elif item.type_id.item_type == "selection":
|
||||||
|
content = []
|
||||||
|
for option in item.option_ids:
|
||||||
|
if option.id != int(value):
|
||||||
|
content.append("<strike>%s</strike>" % (option.value))
|
||||||
|
else:
|
||||||
|
content.append(option.value)
|
||||||
|
font_size = height * normalFontSize * 0.8
|
||||||
|
text = " / ".join(content)
|
||||||
|
string_width = stringWidth(text.replace("<strike>", "").replace("</strike>", ""), font, font_size)
|
||||||
|
p = Paragraph(text, ParagraphStyle(name='Selection Paragraph', fontName=font, fontSize=font_size, leading=12))
|
||||||
|
posX = width * (item.posX + item.width * 0.5) - string_width // 2
|
||||||
|
posY = height * (1 - item.posY - item.height * 0.5) - p.wrap(width, height)[1] // 2
|
||||||
|
p.drawOn(can, posX, posY)
|
||||||
|
|
||||||
|
elif item.type_id.item_type == "textarea":
|
||||||
|
font_size = height * normalFontSize * 0.8
|
||||||
|
can.setFont(font, font_size)
|
||||||
|
lines = value.split('\n')
|
||||||
|
y = (1-item.posY)
|
||||||
|
for line in lines:
|
||||||
|
empty_space = width * item.width - can.stringWidth(line, font, font_size)
|
||||||
|
x_shift = 0
|
||||||
|
if item.alignment == 'center':
|
||||||
|
x_shift = empty_space / 2
|
||||||
|
elif item.alignment == 'right':
|
||||||
|
x_shift = empty_space
|
||||||
|
y -= normalFontSize * 0.9
|
||||||
|
line = reshape_text(line)
|
||||||
|
can.drawString(width * item.posX + x_shift, height * y, line)
|
||||||
|
y -= normalFontSize * 0.1
|
||||||
|
|
||||||
|
elif item.type_id.item_type == "checkbox":
|
||||||
|
can.setFont(font, height*item.height*0.8)
|
||||||
|
value = 'X' if value == 'on' else ''
|
||||||
|
can.drawString(width*item.posX, height*(1-item.posY-item.height*0.9), value)
|
||||||
|
elif item.type_id.item_type == "radio":
|
||||||
|
x = width * item.posX
|
||||||
|
y = height * (1 - item.posY)
|
||||||
|
w = item.width * width
|
||||||
|
h = item.height * height
|
||||||
|
# Calculate the center of the sign item rectangle.
|
||||||
|
c_x = x + w * 0.5
|
||||||
|
c_y = y - h * 0.5
|
||||||
|
# Draw the outer empty circle.
|
||||||
|
can.circle(c_x, c_y, h * 0.5)
|
||||||
|
if value == "on":
|
||||||
|
# Draw the inner filled circle.
|
||||||
|
can.circle(x_cen=c_x, y_cen=c_y, r=h * 0.5 * 0.75, fill=1)
|
||||||
|
elif item.type_id.item_type == "signature" or item.type_id.item_type == "initial":
|
||||||
|
try:
|
||||||
|
image_reader = ImageReader(io.BytesIO(base64.b64decode(value[value.find(',')+1:])))
|
||||||
|
except UnidentifiedImageError:
|
||||||
|
raise ValidationError(_("There was an issue downloading your document. Please contact an administrator."))
|
||||||
|
_fix_image_transparency(image_reader._image)
|
||||||
|
can.drawImage(image_reader, width*item.posX, height*(1-item.posY-item.height), width*item.width, height*item.height, 'auto', True)
|
||||||
|
|
||||||
|
can.showPage()
|
||||||
|
|
||||||
|
can.save()
|
||||||
|
|
||||||
|
item_pdf = PdfFileReader(packet, overwriteWarnings=False)
|
||||||
|
new_pdf = PdfFileWriter()
|
||||||
|
|
||||||
|
for p in range(0, old_pdf.getNumPages()):
|
||||||
|
page = old_pdf.getPage(p)
|
||||||
|
page.mergePage(item_pdf.getPage(p))
|
||||||
|
new_pdf.addPage(page)
|
||||||
|
|
||||||
|
if isEncrypted:
|
||||||
|
new_pdf.encrypt(password)
|
||||||
|
|
||||||
|
try:
|
||||||
|
output = io.BytesIO()
|
||||||
|
new_pdf.write(output)
|
||||||
|
except PdfReadError:
|
||||||
|
raise ValidationError(_("There was an issue downloading your document. Please contact an administrator."))
|
||||||
|
|
||||||
|
self.completed_document = base64.b64encode(output.getvalue())
|
||||||
|
output.close()
|
||||||
|
|
||||||
105
static/src/js/sign_backend_patch.js
Normal file
105
static/src/js/sign_backend_patch.js
Normal file
@ -0,0 +1,105 @@
|
|||||||
|
/** @odoo-module **/
|
||||||
|
|
||||||
|
import { SignTemplateIframe } from "@sign/backend_components/sign_template/sign_template_iframe";
|
||||||
|
import { SignTemplateBody } from "@sign/backend_components/sign_template/sign_template_body";
|
||||||
|
// IMPORTANT: We must import SignItemCustomPopover to pass it to this.popover.add
|
||||||
|
import { SignItemCustomPopover } from "@sign/backend_components/sign_template/sign_item_custom_popover";
|
||||||
|
import { patch } from "@web/core/utils/patch";
|
||||||
|
|
||||||
|
patch(SignTemplateIframe.prototype, {
|
||||||
|
async openSignItemPopup(signItem) {
|
||||||
|
const shouldOpenNewPopover = !(signItem.data.id in this.closePopoverFns);
|
||||||
|
this.closePopover();
|
||||||
|
if (shouldOpenNewPopover) {
|
||||||
|
if (signItem.data.id in this.negativeIds) {
|
||||||
|
await this.negativeIds[signItem.data.id];
|
||||||
|
}
|
||||||
|
const header_title = signItem.data.type === "radio" ? "Radio Button" : signItem.data.type_id?.[1] || signItem.data.name;
|
||||||
|
const closeFn = this.popover.add(
|
||||||
|
signItem.el,
|
||||||
|
SignItemCustomPopover,
|
||||||
|
{
|
||||||
|
debug: this.env.debug,
|
||||||
|
responsible: signItem.data.responsible,
|
||||||
|
roles: this.signRolesById,
|
||||||
|
alignment: signItem.data.alignment,
|
||||||
|
required: signItem.data.required,
|
||||||
|
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,
|
||||||
|
radio_set_id: signItem.data.radio_set_id,
|
||||||
|
// PATCH START
|
||||||
|
sequence_id: signItem.data.sequence_id,
|
||||||
|
sequence_prefix: signItem.data.sequence_prefix,
|
||||||
|
sequence_padding: signItem.data.sequence_padding,
|
||||||
|
sequence_number_next: signItem.data.sequence_number_next,
|
||||||
|
// PATCH END
|
||||||
|
onValidate: (data) => {
|
||||||
|
this.updateSignItem(signItem, data);
|
||||||
|
this.closePopover();
|
||||||
|
},
|
||||||
|
onDelete: () => {
|
||||||
|
this.closePopover();
|
||||||
|
this.deleteSignItem(signItem);
|
||||||
|
},
|
||||||
|
onClose: () => {
|
||||||
|
this.closePopover();
|
||||||
|
},
|
||||||
|
updateSelectionOptions: (ids) => this.updateSelectionOptions(ids),
|
||||||
|
updateRoles: (id) => this.updateRoles(id),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
position: "right",
|
||||||
|
onClose: () => {
|
||||||
|
this.closePopoverFns = {};
|
||||||
|
},
|
||||||
|
closeOnClickAway: (target) => !target.closest(".modal"),
|
||||||
|
popoverClass: "sign-popover",
|
||||||
|
}
|
||||||
|
);
|
||||||
|
this.closePopoverFns[signItem.data.id] = {
|
||||||
|
close: closeFn,
|
||||||
|
signItem,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
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'];
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return super.updateSignItem(signItem, data);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
patch(SignTemplateBody.prototype, {
|
||||||
|
prepareTemplateData() {
|
||||||
|
const [updatedSignItems, Id2UpdatedItem] = super.prepareTemplateData();
|
||||||
|
const items = this.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;
|
||||||
|
}
|
||||||
|
// 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return [updatedSignItems, Id2UpdatedItem];
|
||||||
|
}
|
||||||
|
});
|
||||||
61
static/src/js/sign_item_custom_popover_patch.js
Normal file
61
static/src/js/sign_item_custom_popover_patch.js
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
/** @odoo-module **/
|
||||||
|
|
||||||
|
import { SignItemCustomPopover } from "@sign/backend_components/sign_template/sign_item_custom_popover";
|
||||||
|
import { patch } from "@web/core/utils/patch";
|
||||||
|
|
||||||
|
patch(SignItemCustomPopover.prototype, {
|
||||||
|
setup() {
|
||||||
|
super.setup();
|
||||||
|
// Add sequence fields to the loaded fields
|
||||||
|
this.signItemFieldsGet.sequence_id = { type: "many2one", relation: "ir.sequence", string: "Sequence" };
|
||||||
|
this.signItemFieldsGet.sequence_prefix = { type: "char", string: "Prefix" };
|
||||||
|
this.signItemFieldsGet.sequence_padding = { type: "integer", string: "Padding" };
|
||||||
|
this.signItemFieldsGet.sequence_number_next = { type: "integer", string: "Next Number" };
|
||||||
|
|
||||||
|
// Initialize state from props
|
||||||
|
this.state.sequence_id = this.props.sequence_id;
|
||||||
|
this.state.sequence_prefix = this.props.sequence_prefix;
|
||||||
|
this.state.sequence_padding = this.props.sequence_padding;
|
||||||
|
this.state.sequence_number_next = this.props.sequence_number_next;
|
||||||
|
},
|
||||||
|
|
||||||
|
get recordProps() {
|
||||||
|
// We need to intercept onRecordChanged to sync changes to this.state
|
||||||
|
const originalRecordProps = super.recordProps;
|
||||||
|
const originalOnRecordChanged = originalRecordProps.onRecordChanged;
|
||||||
|
|
||||||
|
return {
|
||||||
|
...originalRecordProps,
|
||||||
|
onRecordChanged: async (record, changes) => {
|
||||||
|
if (originalOnRecordChanged) {
|
||||||
|
await originalOnRecordChanged(record, changes);
|
||||||
|
}
|
||||||
|
// Sync sequence fields to state if changed
|
||||||
|
if ("sequence_id" in changes) {
|
||||||
|
this.state.sequence_id = changes.sequence_id;
|
||||||
|
// When sequence changes, we might want to reload prefix/padding/etc if they are computed?
|
||||||
|
// They are related fields, so 'record' should have them updated.
|
||||||
|
// But 'changes' might only contain the changed field.
|
||||||
|
// We should check the record data for the others.
|
||||||
|
|
||||||
|
// Actually, if they are related fields, the `Record` model updates them.
|
||||||
|
// But `changes` object passed here contains changed values.
|
||||||
|
}
|
||||||
|
if ("sequence_prefix" in changes) this.state.sequence_prefix = changes.sequence_prefix;
|
||||||
|
if ("sequence_padding" in changes) this.state.sequence_padding = changes.sequence_padding;
|
||||||
|
if ("sequence_number_next" in changes) this.state.sequence_number_next = changes.sequence_number_next;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
import { CharField } from "@web/views/fields/char/char_field";
|
||||||
|
import { IntegerField } from "@web/views/fields/integer/integer_field";
|
||||||
|
|
||||||
|
// Patch static components
|
||||||
|
const originalComponents = SignItemCustomPopover.components;
|
||||||
|
SignItemCustomPopover.components = {
|
||||||
|
...originalComponents,
|
||||||
|
CharField,
|
||||||
|
IntegerField,
|
||||||
|
};
|
||||||
49
static/src/xml/sign_item_custom_popover_patch.xml
Normal file
49
static/src/xml/sign_item_custom_popover_patch.xml
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<templates xml:space="preserve">
|
||||||
|
<t t-name="sign.SignItemCustomPopover.sequence" t-inherit="sign.SignItemCustomPopover" t-inherit-mode="extension">
|
||||||
|
<xpath expr="//div[hasclass('d-flex', 'p-2', 'flex-column')]/div[1]" position="before">
|
||||||
|
<!-- Insert inside the main column, before the placeholder input -->
|
||||||
|
<div t-if="props.type === 'sequence'" class="mb-3 border-bottom pb-3">
|
||||||
|
<label class="fw-bold mb-2">Sequence Settings</label>
|
||||||
|
|
||||||
|
<!-- We need to access the record from the scope, but the scope is inside <Record> component -->
|
||||||
|
<!-- The Record component is below this div in original template? -->
|
||||||
|
<!-- Wait, the original template has <div t-if="props.debug"> then <Record> -->
|
||||||
|
<!-- I should insert INSIDE the <Record> component to access the `record` variable -->
|
||||||
|
</div>
|
||||||
|
</xpath>
|
||||||
|
|
||||||
|
<xpath expr="//div[@id='o_sign_responsible_select_input']/.." position="after">
|
||||||
|
<t t-if="props.type === 'sequence'">
|
||||||
|
<div class="mb-2">
|
||||||
|
<label class="fw-bold">Sequence</label>
|
||||||
|
<div class="o_field_widget d-block">
|
||||||
|
<Many2OneField t-props="getMany2XProps(record, 'sequence_id')" update="(value) => record.update(value)"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- Only show details if sequence is selected -->
|
||||||
|
<!-- Note: record.data.sequence_id is an array [id, display_name] usually in M2O -->
|
||||||
|
<t t-if="record.data.sequence_id">
|
||||||
|
<div class="mb-2">
|
||||||
|
<label class="fw-bold">Prefix</label>
|
||||||
|
<div class="o_field_widget d-block">
|
||||||
|
<CharField name="'sequence_prefix'" record="record" readonly="false"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="mb-2">
|
||||||
|
<label class="fw-bold">Next Number</label>
|
||||||
|
<div class="o_field_widget d-block">
|
||||||
|
<IntegerField name="'sequence_number_next'" record="record" readonly="false"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="mb-2">
|
||||||
|
<label class="fw-bold">Padding</label>
|
||||||
|
<div class="o_field_widget d-block">
|
||||||
|
<IntegerField name="'sequence_padding'" record="record" readonly="false"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</t>
|
||||||
|
</t>
|
||||||
|
</xpath>
|
||||||
|
</t>
|
||||||
|
</templates>
|
||||||
38
static/src/xml/sign_item_sequence.xml
Normal file
38
static/src/xml/sign_item_sequence.xml
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<templates xml:space="preserve">
|
||||||
|
<t t-name="sign.signItem" t-inherit="sign.signItem" t-inherit-mode="extension">
|
||||||
|
<xpath expr="//t[@t-if="type == 'selection'"]" position="after">
|
||||||
|
<t t-if="type == 'sequence'" t-call="sign.sequenceSignItem"/>
|
||||||
|
</xpath>
|
||||||
|
<!-- This second xpath seems to be for the editor drag-drop preview or specific state -->
|
||||||
|
<xpath expr="//div[@t-if="type == 'selection'"]" position="after">
|
||||||
|
<div t-if="type == 'sequence'" t-att-title="role" t-attf-class="{{classes}} o_sign_sign_item" t-att-style="style" t-att-data-value="value" style="text-align:center; display:flex; align-items:center; justify-content:center;">
|
||||||
|
<t t-if="value">
|
||||||
|
<span t-esc="value" style="font-family:monospace;"/>
|
||||||
|
</t>
|
||||||
|
<t t-else="">
|
||||||
|
<span class="o_placeholder">
|
||||||
|
<t t-esc="placeholder"/>
|
||||||
|
</span>
|
||||||
|
</t>
|
||||||
|
<t t-if="isSignItemEditable" t-call="sign.signItemConfiguration"/>
|
||||||
|
</div>
|
||||||
|
</xpath>
|
||||||
|
</t>
|
||||||
|
|
||||||
|
<t t-name="sign.sequenceSignItem">
|
||||||
|
<div t-att-title="role" t-attf-class="{{classes}} o_sign_sign_item" t-att-data-id="id" t-att-style="style" style="text-align:center; display:flex; align-items:center; justify-content:center;">
|
||||||
|
<div class="sign_item_body">
|
||||||
|
<t t-if="value">
|
||||||
|
<span t-esc="value" style="font-family:monospace;"/>
|
||||||
|
</t>
|
||||||
|
<t t-else="">
|
||||||
|
<span class="o_placeholder">
|
||||||
|
<t t-esc="placeholder"/>
|
||||||
|
</span>
|
||||||
|
</t>
|
||||||
|
<t t-if="editMode || isSignItemEditable" t-call="sign.signItemConfiguration"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</t>
|
||||||
|
</templates>
|
||||||
17
views/sign_item_views.xml
Normal file
17
views/sign_item_views.xml
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<odoo>
|
||||||
|
<record id="sign_item_view_form" model="ir.ui.view">
|
||||||
|
<field name="name">sign.item.view.form.inherit.sequence</field>
|
||||||
|
<field name="model">sign.item</field>
|
||||||
|
<field name="inherit_id" ref="sign.sign_item_view_form"/>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<xpath expr="//field[@name='type_id']" position="after">
|
||||||
|
<field name="item_type" invisible="1"/>
|
||||||
|
<field name="sequence_id" invisible="item_type != 'sequence'"/>
|
||||||
|
<field name="sequence_prefix" invisible="item_type != 'sequence'"/>
|
||||||
|
<field name="sequence_padding" invisible="item_type != 'sequence'"/>
|
||||||
|
<field name="sequence_number_next" invisible="item_type != 'sequence'"/>
|
||||||
|
</xpath>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
</odoo>
|
||||||
Loading…
Reference in New Issue
Block a user