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