Files
Odoo-Modules/fusion_payroll/models/cheque_layout_settings.py
2026-02-22 01:22:18 -05:00

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.'))