507 lines
23 KiB
Python
507 lines
23 KiB
Python
# -*- coding: utf-8 -*-
|
|
"""
|
|
Cheque Layout Settings
|
|
======================
|
|
Configurable cheque layout with visual preview.
|
|
"""
|
|
|
|
from odoo import api, fields, models, _
|
|
from odoo.exceptions import UserError
|
|
import base64
|
|
|
|
|
|
class ChequeLayoutSettings(models.Model):
|
|
"""
|
|
Cheque Layout Settings with XY positioning for all fields.
|
|
Includes image upload for visual preview alignment.
|
|
"""
|
|
_name = 'cheque.layout.settings'
|
|
_description = 'Cheque Layout Settings'
|
|
_rec_name = 'name'
|
|
|
|
name = fields.Char(string='Layout Name', required=True, default='Default Layout')
|
|
active = fields.Boolean(default=True)
|
|
is_default = fields.Boolean(string='Default Layout', default=False)
|
|
company_id = fields.Many2one(
|
|
'res.company',
|
|
string='Company',
|
|
default=lambda self: self.env.company,
|
|
required=True,
|
|
)
|
|
|
|
# Background Image for Preview
|
|
cheque_image = fields.Binary(
|
|
string='Cheque Background Image',
|
|
help='Upload your pre-printed cheque stock image for alignment preview',
|
|
)
|
|
cheque_image_filename = fields.Char(string='Cheque Image Filename')
|
|
|
|
# =============== PAGE SETTINGS ===============
|
|
page_width = fields.Float(string='Page Width (inches)', default=8.5)
|
|
page_height = fields.Float(string='Page Height (inches)', default=11.0)
|
|
|
|
# =============== SECTION HEIGHTS ===============
|
|
# All values in inches from top of page
|
|
section1_height = fields.Float(
|
|
string='Cheque Section Height (in)',
|
|
default=3.67,
|
|
help='Height of the cheque section (top section)',
|
|
)
|
|
section2_start = fields.Float(
|
|
string='Stub 1 Start Position (in)',
|
|
default=3.67,
|
|
help='Y position where stub 1 begins',
|
|
)
|
|
section2_height = fields.Float(
|
|
string='Stub 1 Height (in)',
|
|
default=3.67,
|
|
help='Height of stub 1 (middle section)',
|
|
)
|
|
section3_start = fields.Float(
|
|
string='Stub 2 Start Position (in)',
|
|
default=7.34,
|
|
help='Y position where stub 2 begins',
|
|
)
|
|
section3_height = fields.Float(
|
|
string='Stub 2 Height (in)',
|
|
default=3.66,
|
|
help='Height of stub 2 (bottom section)',
|
|
)
|
|
|
|
# =============== CHEQUE SECTION POSITIONS ===============
|
|
# Date
|
|
date_x = fields.Float(string='Date X (in)', default=6.8)
|
|
date_y = fields.Float(string='Date Y (in)', default=0.35)
|
|
date_font_size = fields.Integer(string='Date Font Size (pt)', default=11)
|
|
|
|
# Amount (numeric)
|
|
amount_x = fields.Float(string='Amount X (in)', default=6.9)
|
|
amount_y = fields.Float(string='Amount Y (in)', default=0.75)
|
|
amount_font_size = fields.Integer(string='Amount Font Size (pt)', default=12)
|
|
|
|
# Amount in Words
|
|
amount_words_x = fields.Float(string='Amount Words X (in)', default=0.6)
|
|
amount_words_y = fields.Float(string='Amount Words Y (in)', default=1.25)
|
|
amount_words_font_size = fields.Integer(string='Amount Words Font Size (pt)', default=9)
|
|
|
|
# Payee Name
|
|
payee_x = fields.Float(string='Payee Name X (in)', default=0.6)
|
|
payee_y = fields.Float(string='Payee Name Y (in)', default=1.65)
|
|
payee_font_size = fields.Integer(string='Payee Font Size (pt)', default=11)
|
|
|
|
# Payee Address (separate from name)
|
|
payee_address_x = fields.Float(string='Payee Address X (in)', default=0.6)
|
|
payee_address_y = fields.Float(string='Payee Address Y (in)', default=1.85)
|
|
payee_address_font_size = fields.Integer(string='Payee Address Font Size (pt)', default=9)
|
|
|
|
# Pay Period (on cheque)
|
|
cheque_pay_period_x = fields.Float(string='Pay Period X (in)', default=0.6)
|
|
cheque_pay_period_y = fields.Float(string='Pay Period Y (in)', default=2.1)
|
|
|
|
# Memo
|
|
memo_x = fields.Float(string='Memo X (in)', default=0.6)
|
|
memo_y = fields.Float(string='Memo Y (in)', default=3.1)
|
|
memo_font_size = fields.Integer(string='Memo Font Size (pt)', default=8)
|
|
|
|
# =============== STUB SECTION OFFSETS ===============
|
|
# These are relative to the section start position
|
|
stub_padding_top = fields.Float(string='Stub Top Padding (in)', default=0.35)
|
|
stub_padding_left = fields.Float(string='Stub Left Padding (in)', default=0.1)
|
|
stub_padding_right = fields.Float(string='Stub Right Padding (in)', default=0.1)
|
|
stub_padding_bottom = fields.Float(string='Stub Bottom Padding (in)', default=0.1)
|
|
stub_full_width = fields.Boolean(string='Use Full Page Width', default=True, help='Stub content uses full page width')
|
|
stub_center_data = fields.Boolean(string='Center Data in Stub', default=True, help='Center the stub data horizontally')
|
|
stub_content_margin = fields.Float(string='Stub Content Margin (in)', default=0.5, help='Left and right margin for stub content when centering. Increase to push content more toward center.')
|
|
|
|
# Column widths (as percentages)
|
|
col1_width = fields.Integer(string='Column 1 Width (%)', default=22, help='Employee/Company Info')
|
|
col2_width = fields.Integer(string='Column 2 Width (%)', default=26, help='PAY/Benefits')
|
|
col3_width = fields.Integer(string='Column 3 Width (%)', default=26, help='Taxes/Deductions')
|
|
col4_width = fields.Integer(string='Column 4 Width (%)', default=26, help='Summary')
|
|
|
|
# Font sizes for stubs
|
|
stub_title_font_size = fields.Integer(string='Stub Title Font Size', default=9)
|
|
stub_content_font_size = fields.Integer(string='Stub Content Font Size', default=7)
|
|
stub_header_font_size = fields.Integer(string='Stub Header Font Size', default=6)
|
|
|
|
# =============== METHODS ===============
|
|
|
|
@api.model
|
|
def get_default_settings(self):
|
|
"""Get or create the default layout settings for the current company."""
|
|
settings = self.search([
|
|
('company_id', '=', self.env.company.id),
|
|
('is_default', '=', True),
|
|
], limit=1)
|
|
|
|
if not settings:
|
|
settings = self.search([
|
|
('company_id', '=', self.env.company.id),
|
|
], limit=1)
|
|
|
|
if not settings:
|
|
settings = self.create({
|
|
'name': 'Default Cheque Layout',
|
|
'company_id': self.env.company.id,
|
|
'is_default': True,
|
|
})
|
|
|
|
return settings
|
|
|
|
def action_set_as_default(self):
|
|
"""Set this layout as the default for the company."""
|
|
# Unset other defaults
|
|
self.search([
|
|
('company_id', '=', self.company_id.id),
|
|
('is_default', '=', True),
|
|
('id', '!=', self.id),
|
|
]).write({'is_default': False})
|
|
|
|
self.is_default = True
|
|
|
|
def get_layout_data(self):
|
|
"""Return all layout settings as a dictionary for the report template."""
|
|
self.ensure_one()
|
|
return {
|
|
'page': {
|
|
'width': self.page_width,
|
|
'height': self.page_height,
|
|
},
|
|
'sections': {
|
|
'cheque': {
|
|
'height': self.section1_height,
|
|
},
|
|
'stub1': {
|
|
'start': self.section2_start,
|
|
'height': self.section2_height,
|
|
},
|
|
'stub2': {
|
|
'start': self.section3_start,
|
|
'height': self.section3_height,
|
|
},
|
|
},
|
|
'cheque': {
|
|
'date': {'x': self.date_x, 'y': self.date_y, 'font_size': self.date_font_size},
|
|
'amount': {'x': self.amount_x, 'y': self.amount_y, 'font_size': self.amount_font_size},
|
|
'amount_words': {'x': self.amount_words_x, 'y': self.amount_words_y, 'font_size': self.amount_words_font_size},
|
|
'payee': {'x': self.payee_x, 'y': self.payee_y, 'font_size': self.payee_font_size},
|
|
'pay_period': {'x': self.cheque_pay_period_x, 'y': self.cheque_pay_period_y},
|
|
'memo': {'x': self.memo_x, 'y': self.memo_y, 'font_size': self.memo_font_size},
|
|
},
|
|
'stub': {
|
|
'padding': {
|
|
'top': self.stub_padding_top,
|
|
'left': self.stub_padding_left,
|
|
'right': self.stub_padding_right,
|
|
'bottom': self.stub_padding_bottom,
|
|
},
|
|
'columns': {
|
|
'col1': self.col1_width,
|
|
'col2': self.col2_width,
|
|
'col3': self.col3_width,
|
|
'col4': self.col4_width,
|
|
},
|
|
'fonts': {
|
|
'title': self.stub_title_font_size,
|
|
'content': self.stub_content_font_size,
|
|
'header': self.stub_header_font_size,
|
|
},
|
|
},
|
|
}
|
|
|
|
def action_open_preview(self):
|
|
"""Open the visual preview configuration wizard."""
|
|
self.ensure_one()
|
|
# Ensure the record is saved to database (required for related fields to work)
|
|
# Check if it's a real database ID (integer) vs a NewId
|
|
if not isinstance(self.id, int):
|
|
# Record not yet saved - need to save first
|
|
raise UserError(_('Please save the record first before opening the preview.'))
|
|
|
|
return {
|
|
'type': 'ir.actions.act_window',
|
|
'name': _('Cheque Layout Preview'),
|
|
'res_model': 'cheque.layout.preview.wizard',
|
|
'view_mode': 'form',
|
|
'target': 'new',
|
|
'context': {
|
|
'default_settings_id': self.id,
|
|
},
|
|
}
|
|
|
|
|
|
class ChequeLayoutPreviewWizard(models.TransientModel):
|
|
"""
|
|
Wizard for visual preview of cheque layout.
|
|
Shows the cheque image with overlaid field positions.
|
|
"""
|
|
_name = 'cheque.layout.preview.wizard'
|
|
_description = 'Cheque Layout Preview'
|
|
|
|
settings_id = fields.Many2one('cheque.layout.settings', string='Layout Settings', required=True)
|
|
|
|
# Local copies of fields for editing (will be saved back to settings on close)
|
|
cheque_image = fields.Binary(string='Cheque Background Image')
|
|
cheque_image_filename = fields.Char(string='Cheque Image Filename')
|
|
|
|
# Section positions
|
|
section1_height = fields.Float(string='Cheque End (in)', default=3.67)
|
|
section2_start = fields.Float(string='Stub 1 Start (in)', default=3.67)
|
|
section3_start = fields.Float(string='Stub 2 Start (in)', default=7.34)
|
|
|
|
# Cheque field positions
|
|
date_x = fields.Float(string='Date X', default=6.8)
|
|
date_y = fields.Float(string='Date Y', default=0.35)
|
|
amount_x = fields.Float(string='Amount X', default=6.9)
|
|
amount_y = fields.Float(string='Amount Y', default=0.75)
|
|
amount_words_x = fields.Float(string='Amount Words X', default=0.6)
|
|
amount_words_y = fields.Float(string='Amount Words Y', default=1.25)
|
|
payee_x = fields.Float(string='Payee X', default=0.6)
|
|
payee_y = fields.Float(string='Payee Y', default=1.65)
|
|
payee_address_x = fields.Float(string='Payee Address X', default=0.6)
|
|
payee_address_y = fields.Float(string='Payee Address Y', default=1.85)
|
|
cheque_pay_period_x = fields.Float(string='Pay Period X', default=0.6)
|
|
cheque_pay_period_y = fields.Float(string='Pay Period Y', default=2.1)
|
|
memo_x = fields.Float(string='Memo X', default=0.6)
|
|
memo_y = fields.Float(string='Memo Y', default=3.1)
|
|
|
|
# Stub positions
|
|
stub_padding_top = fields.Float(string='Stub Top Padding', default=0.35)
|
|
stub_padding_left = fields.Float(string='Stub Left Padding', default=0.1)
|
|
stub_content_margin = fields.Float(string='Stub Content Margin (in)', default=0.5)
|
|
stub_center_data = fields.Boolean(string='Center Data in Stub', default=True)
|
|
|
|
# Preview HTML (computed)
|
|
preview_html = fields.Html(string='Preview', compute='_compute_preview_html', sanitize=False)
|
|
|
|
@api.model
|
|
def default_get(self, fields_list):
|
|
"""Load values from settings_id when wizard is opened."""
|
|
res = super().default_get(fields_list)
|
|
settings_id = self.env.context.get('default_settings_id')
|
|
if settings_id:
|
|
settings = self.env['cheque.layout.settings'].browse(settings_id)
|
|
if settings.exists():
|
|
res.update({
|
|
'cheque_image': settings.cheque_image,
|
|
'cheque_image_filename': settings.cheque_image_filename,
|
|
'section1_height': settings.section1_height,
|
|
'section2_start': settings.section2_start,
|
|
'section3_start': settings.section3_start,
|
|
'date_x': settings.date_x,
|
|
'date_y': settings.date_y,
|
|
'amount_x': settings.amount_x,
|
|
'amount_y': settings.amount_y,
|
|
'amount_words_x': settings.amount_words_x,
|
|
'amount_words_y': settings.amount_words_y,
|
|
'payee_x': settings.payee_x,
|
|
'payee_y': settings.payee_y,
|
|
'payee_address_x': settings.payee_address_x,
|
|
'payee_address_y': settings.payee_address_y,
|
|
'cheque_pay_period_x': settings.cheque_pay_period_x,
|
|
'cheque_pay_period_y': settings.cheque_pay_period_y,
|
|
'memo_x': settings.memo_x,
|
|
'memo_y': settings.memo_y,
|
|
'stub_padding_top': settings.stub_padding_top,
|
|
'stub_padding_left': settings.stub_padding_left,
|
|
'stub_content_margin': settings.stub_content_margin,
|
|
'stub_center_data': settings.stub_center_data,
|
|
})
|
|
return res
|
|
|
|
@api.depends(
|
|
'cheque_image', 'section1_height', 'section2_start', 'section3_start',
|
|
'date_x', 'date_y', 'amount_x', 'amount_y', 'amount_words_x', 'amount_words_y',
|
|
'payee_x', 'payee_y', 'payee_address_x', 'payee_address_y',
|
|
'cheque_pay_period_x', 'cheque_pay_period_y',
|
|
'memo_x', 'memo_y', 'stub_padding_top', 'stub_padding_left'
|
|
)
|
|
def _compute_preview_html(self):
|
|
"""Generate HTML preview with overlaid positions."""
|
|
for wizard in self:
|
|
# Scale factor: 1 inch = 72 pixels for preview
|
|
scale = 72
|
|
page_width = 8.5 * scale
|
|
page_height = 11 * scale
|
|
|
|
# Build background image style
|
|
bg_style = ''
|
|
if wizard.cheque_image:
|
|
image_data = wizard.cheque_image.decode('utf-8') if isinstance(wizard.cheque_image, bytes) else wizard.cheque_image
|
|
bg_style = f'background-image: url(data:image/png;base64,{image_data}); background-size: 100% 100%;'
|
|
|
|
# Helper to convert inches to pixels
|
|
def px(inches):
|
|
return (inches or 0) * scale
|
|
|
|
# Section divider lines
|
|
section_lines = f'''
|
|
<div style="position: absolute; top: {px(wizard.section1_height)}px; left: 0; right: 0;
|
|
border-bottom: 2px dashed red; z-index: 10;">
|
|
<span style="background: red; color: white; font-size: 10px; padding: 2px 4px;">
|
|
Cheque End ({wizard.section1_height:.2f} in)
|
|
</span>
|
|
</div>
|
|
<div style="position: absolute; top: {px(wizard.section3_start)}px; left: 0; right: 0;
|
|
border-bottom: 2px dashed red; z-index: 10;">
|
|
<span style="background: red; color: white; font-size: 10px; padding: 2px 4px;">
|
|
Stub 2 Start ({wizard.section3_start:.2f} in)
|
|
</span>
|
|
</div>
|
|
'''
|
|
|
|
# Field markers (cheque section)
|
|
field_markers = f'''
|
|
<!-- Date -->
|
|
<div style="position: absolute; left: {px(wizard.date_x)}px; top: {px(wizard.date_y)}px;
|
|
background: rgba(0,128,255,0.9); color: white; padding: 2px 6px; font-size: 10px;
|
|
border: 1px solid blue; z-index: 20; white-space: nowrap;">
|
|
DATE: JAN 11, 2026
|
|
</div>
|
|
|
|
<!-- Amount -->
|
|
<div style="position: absolute; left: {px(wizard.amount_x)}px; top: {px(wizard.amount_y)}px;
|
|
background: rgba(0,180,0,0.9); color: white; padding: 2px 6px; font-size: 10px;
|
|
border: 1px solid green; z-index: 20; white-space: nowrap;">
|
|
**1,465.19
|
|
</div>
|
|
|
|
<!-- Amount in Words -->
|
|
<div style="position: absolute; left: {px(wizard.amount_words_x)}px; top: {px(wizard.amount_words_y)}px;
|
|
background: rgba(255,128,0,0.9); color: white; padding: 2px 6px; font-size: 9px;
|
|
border: 1px solid orange; z-index: 20; white-space: nowrap;">
|
|
AMOUNT IN WORDS
|
|
</div>
|
|
|
|
<!-- Payee Name -->
|
|
<div style="position: absolute; left: {px(wizard.payee_x)}px; top: {px(wizard.payee_y)}px;
|
|
background: rgba(128,0,255,0.9); color: white; padding: 2px 6px; font-size: 10px;
|
|
border: 1px solid purple; z-index: 20; white-space: nowrap;">
|
|
PAYEE NAME
|
|
</div>
|
|
|
|
<!-- Payee Address -->
|
|
<div style="position: absolute; left: {px(wizard.payee_address_x)}px; top: {px(wizard.payee_address_y)}px;
|
|
background: rgba(180,0,180,0.9); color: white; padding: 2px 6px; font-size: 9px;
|
|
border: 1px solid #b400b4; z-index: 20; white-space: nowrap;">
|
|
PAYEE ADDRESS
|
|
</div>
|
|
|
|
<!-- Pay Period -->
|
|
<div style="position: absolute; left: {px(wizard.cheque_pay_period_x)}px; top: {px(wizard.cheque_pay_period_y)}px;
|
|
background: rgba(255,0,128,0.9); color: white; padding: 2px 6px; font-size: 9px;
|
|
border: 1px solid #ff0080; z-index: 20; white-space: nowrap;">
|
|
PAY PERIOD
|
|
</div>
|
|
|
|
<!-- Memo -->
|
|
<div style="position: absolute; left: {px(wizard.memo_x)}px; top: {px(wizard.memo_y)}px;
|
|
background: rgba(128,128,128,0.9); color: white; padding: 2px 6px; font-size: 9px;
|
|
border: 1px solid gray; z-index: 20; white-space: nowrap;">
|
|
MEMO
|
|
</div>
|
|
'''
|
|
|
|
# Stub markers - full width centered
|
|
stub1_top = (wizard.section2_start or 0) + (wizard.stub_padding_top or 0)
|
|
stub2_top = (wizard.section3_start or 0) + (wizard.stub_padding_top or 0)
|
|
|
|
stub_markers = f'''
|
|
<!-- Stub 1 Content Area (Full Width, Centered) -->
|
|
<div style="position: absolute; left: {px(wizard.stub_padding_left)}px; top: {px(stub1_top)}px;
|
|
right: {px(wizard.stub_padding_left)}px; height: 200px;
|
|
border: 2px dashed #00aa00; z-index: 15; display: flex; align-items: flex-start; justify-content: center;">
|
|
<span style="background: #00aa00; color: white; font-size: 10px; padding: 2px 6px;">
|
|
STUB 1 - FULL WIDTH CENTERED
|
|
</span>
|
|
</div>
|
|
|
|
<!-- Stub 2 Content Area (Full Width, Centered) -->
|
|
<div style="position: absolute; left: {px(wizard.stub_padding_left)}px; top: {px(stub2_top)}px;
|
|
right: {px(wizard.stub_padding_left)}px; height: 200px;
|
|
border: 2px dashed #0000aa; z-index: 15; display: flex; align-items: flex-start; justify-content: center;">
|
|
<span style="background: #0000aa; color: white; font-size: 10px; padding: 2px 6px;">
|
|
STUB 2 - FULL WIDTH CENTERED
|
|
</span>
|
|
</div>
|
|
'''
|
|
|
|
wizard.preview_html = f'''
|
|
<div style="position: relative; width: {page_width}px; height: {page_height}px;
|
|
border: 1px solid #ccc; {bg_style} overflow: hidden; margin: 0 auto; background-color: #f8f8f8;">
|
|
{section_lines}
|
|
{field_markers}
|
|
{stub_markers}
|
|
</div>
|
|
'''
|
|
|
|
def action_save_and_close(self):
|
|
"""Save changes back to settings and close the wizard."""
|
|
self.ensure_one()
|
|
if self.settings_id:
|
|
self.settings_id.write({
|
|
'cheque_image': self.cheque_image,
|
|
'cheque_image_filename': self.cheque_image_filename,
|
|
'section1_height': self.section1_height,
|
|
'section2_start': self.section2_start,
|
|
'section3_start': self.section3_start,
|
|
'date_x': self.date_x,
|
|
'date_y': self.date_y,
|
|
'amount_x': self.amount_x,
|
|
'amount_y': self.amount_y,
|
|
'amount_words_x': self.amount_words_x,
|
|
'amount_words_y': self.amount_words_y,
|
|
'payee_x': self.payee_x,
|
|
'payee_y': self.payee_y,
|
|
'payee_address_x': self.payee_address_x,
|
|
'payee_address_y': self.payee_address_y,
|
|
'cheque_pay_period_x': self.cheque_pay_period_x,
|
|
'cheque_pay_period_y': self.cheque_pay_period_y,
|
|
'memo_x': self.memo_x,
|
|
'memo_y': self.memo_y,
|
|
'stub_padding_top': self.stub_padding_top,
|
|
'stub_padding_left': self.stub_padding_left,
|
|
'stub_content_margin': self.stub_content_margin,
|
|
'stub_center_data': self.stub_center_data,
|
|
})
|
|
return {'type': 'ir.actions.act_window_close'}
|
|
|
|
def action_print_test(self):
|
|
"""Save current wizard values and print a test cheque."""
|
|
self.ensure_one()
|
|
|
|
# First, save current wizard values to the settings record
|
|
if self.settings_id:
|
|
self.settings_id.write({
|
|
'cheque_image': self.cheque_image,
|
|
'cheque_image_filename': self.cheque_image_filename,
|
|
'section1_height': self.section1_height,
|
|
'section2_start': self.section2_start,
|
|
'section3_start': self.section3_start,
|
|
'date_x': self.date_x,
|
|
'date_y': self.date_y,
|
|
'amount_x': self.amount_x,
|
|
'amount_y': self.amount_y,
|
|
'amount_words_x': self.amount_words_x,
|
|
'amount_words_y': self.amount_words_y,
|
|
'payee_x': self.payee_x,
|
|
'payee_y': self.payee_y,
|
|
'payee_address_x': self.payee_address_x,
|
|
'payee_address_y': self.payee_address_y,
|
|
'cheque_pay_period_x': self.cheque_pay_period_x,
|
|
'cheque_pay_period_y': self.cheque_pay_period_y,
|
|
'memo_x': self.memo_x,
|
|
'memo_y': self.memo_y,
|
|
'stub_padding_top': self.stub_padding_top,
|
|
'stub_padding_left': self.stub_padding_left,
|
|
'stub_content_margin': self.stub_content_margin,
|
|
'stub_center_data': self.stub_center_data,
|
|
})
|
|
|
|
# Find a sample cheque to print
|
|
cheque = self.env['payroll.cheque'].search([], limit=1)
|
|
if cheque:
|
|
return self.env.ref('fusion_payroll.action_report_payroll_cheque').report_action(cheque)
|
|
else:
|
|
raise UserError(_('No cheque records found to print test.'))
|