# -*- 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'''