from odoo import api, fields, models from odoo.exceptions import UserError import logging _logger = logging.getLogger(__name__) class AccountMove(models.Model): _inherit = ['account.move', 'account.move.template.mixin'] move_ids = fields.Many2many( comodel_name='account.move', relation='account_move_template_invoice_move_rel', column1='document_id', column2='move_id', string='Проводки по шаблону', ) journal_template_id = fields.Many2one( comodel_name='account.move.template', string='Шаблон типовой операции', default=lambda self: self._default_journal_template(), ) def _default_journal_template(self): use = self.env['ir.config_parameter'].sudo().get_param( 'account_move_templates_invoice.use_journal_templates_for_invoices' ) if use: return self.env['account.move.template'].search([], limit=1, order='id asc') return False def get_move_line_amounts(self): """Return invoice total as the base amount for the wizard.""" self.ensure_one() return [{'name': self.name or '/', 'amount': self.amount_total}] def _replace_lines_from_template(self, move): """ Replace ALL lines (product + payment_term) with template lines plus one explicit payment_term line with date_maturity. We pass 'balance' explicitly in vals so _get_protected_vals protects it from being overwritten by _sync_invoice. """ _logger.info("TEMPLATE DEBUG: _replace_lines_from_template START for move %s", move.id) template = move.journal_template_id _logger.info("TEMPLATE DEBUG: template = %s", template) base_amount = move.amount_untaxed or move.amount_total _logger.info("TEMPLATE DEBUG: base_amount = %s", base_amount) # Determine receivable/payable account partner = move.commercial_partner_id.with_company(move.company_id) if move.is_sale_document(include_receipts=True): term_account = partner.property_account_receivable_id else: term_account = partner.property_account_payable_id _logger.info("TEMPLATE DEBUG: term_account = %s", term_account) # due date: use invoice_date_due, invoice_date, or today due_date = ( move.invoice_date_due or move.invoice_date or fields.Date.context_today(move) ) _logger.info("TEMPLATE DEBUG: due_date = %s", due_date) # Remove only payment_term lines, keep product lines from invoice_line_ids _logger.info("TEMPLATE DEBUG: Removing existing payment_term lines") lines_to_remove = move.line_ids.filtered(lambda l: l.display_type == 'payment_term') _logger.info("TEMPLATE DEBUG: Lines to remove: %s", [(l.id, l.account_id.code, l.display_type) for l in lines_to_remove]) lines_to_remove.with_context( dynamic_unlink=True, check_move_validity=False, skip_invoice_sync=True, ).unlink() _logger.info("TEMPLATE DEBUG: payment_term lines removed") AML = self.env['account.move.line'] ctx = dict(check_move_validity=False, skip_invoice_sync=True) # 1. Process template lines based on line_type _logger.info("TEMPLATE DEBUG: Processing template lines") # Get existing product lines from invoice_line_ids product_lines = move.line_ids.filtered(lambda l: l.display_type == 'product') _logger.info("TEMPLATE DEBUG: Found %d product lines", len(product_lines)) # Separate template lines by type template_product_lines = template.line_ids.filtered(lambda l: l.line_type == 'product') template_payment_lines = template.line_ids.filtered(lambda l: l.line_type == 'payment') _logger.info("TEMPLATE DEBUG: Template has %d product lines and %d payment lines", len(template_product_lines), len(template_payment_lines)) # Update existing product lines with accounts from template (line_type='product') for idx, product_line in enumerate(product_lines): if idx < len(template_product_lines): tpl_line = template_product_lines[idx] _logger.info("TEMPLATE DEBUG: Updating product line %d: old account=%s, new account=%s", product_line.id, product_line.account_id.code, tpl_line.account_id.code) product_line.with_context(**ctx).write({'account_id': tpl_line.account_id.id}) # Create payment lines from template (line_type='payment') _logger.info("TEMPLATE DEBUG: Creating payment lines from template") for tpl_line in template_payment_lines: line_amount = base_amount * tpl_line.percent / 100.0 balance = line_amount if tpl_line.move_type == 'debit' else -line_amount _logger.info("TEMPLATE DEBUG: Creating payment line: account=%s, balance=%s", tpl_line.account_id.id, balance) vals = { 'move_id': move.id, 'account_id': tpl_line.account_id.id, 'name': template.name, 'balance': balance, 'amount_currency': balance, 'date_maturity': due_date, 'display_type': 'payment_term', } AML.with_context(**ctx).create(vals) _logger.info("TEMPLATE DEBUG: Template lines processed") _logger.info("TEMPLATE DEBUG: _replace_lines_from_template END") def action_post(self): _logger.info("TEMPLATE DEBUG: action_post called for moves: %s", self.ids) use_templates = self.env['ir.config_parameter'].sudo().get_param( 'account_move_templates_invoice.use_journal_templates_for_invoices' ) _logger.info("TEMPLATE DEBUG: use_templates = %s", use_templates) if use_templates: for move in self.filtered( lambda m: m.move_type in ('out_invoice', 'in_invoice') ): _logger.info("TEMPLATE DEBUG: Processing move %s, template: %s", move.id, move.journal_template_id) if not move.journal_template_id: raise UserError( 'Выберите шаблон типовой операции перед проведением.' ) try: _logger.info("TEMPLATE DEBUG: Calling _replace_lines_from_template") self._replace_lines_from_template(move) _logger.info("TEMPLATE DEBUG: _replace_lines_from_template completed") except Exception as e: _logger.error("TEMPLATE DEBUG: Error in _replace_lines_from_template: %s", e, exc_info=True) raise return super().action_post()