Pārlūkot izejas kodu

sale_template_contract updates

Roberto pineda 7 mēneši atpakaļ
vecāks
revīzija
a5bc338d94

+ 1 - 0
sale_template_contract/__manifest__.py

@@ -16,6 +16,7 @@
     'data': [
         'security/ir.model.access.csv',
         'views/sale_order_template_views.xml',
+        'views/project_project_views.xml',
     ],
     'installable': True,
     'application': False,

+ 3 - 1
sale_template_contract/models/__init__.py

@@ -1,4 +1,6 @@
 # -*- coding: utf-8 -*-
 from . import sale_order_template
 from . import sale_order_template_line
-from . import sale_order_line
+from . import sale_order_line
+from . import analytic_line
+from . import project_project

+ 65 - 0
sale_template_contract/models/analytic_line.py

@@ -0,0 +1,65 @@
+from odoo import models, api, fields
+import logging
+
+_logger = logging.getLogger(__name__)
+
+class AccountAnalyticLine(models.Model):
+    _inherit = 'account.analytic.line'
+
+    @api.model_create_multi
+    def create(self, vals_list):
+        records = super().create(vals_list)
+        for record in records:
+            self._stc_auto_link_so_line(self.browse(record.id))
+        return records
+
+    def write(self, vals):
+        res = super().write(vals)
+        for record in self:
+            # Si se está actualizando el empleado o no hay so_line, volver a buscar la línea
+            if 'employee_id' in vals or not record.so_line:
+                self._stc_auto_link_so_line(self.browse(record.id))
+        return res
+
+    def _stc_auto_link_so_line(self, record):
+        _logger.info('[STC] Analizando línea de tiempo ID %s', record.id)
+        if record.project_id:
+            _logger.info('[STC] Proyecto: %s', record.project_id.id)
+            sale_order = record.project_id.reinvoiced_sale_order_id
+            if sale_order:
+                _logger.info('[STC] Proyecto tiene reinvoiced_sale_order_id: %s', sale_order.id)
+                template_lines = self.env['sale.order.template.line'].search([
+                    ('project_id', '=', record.project_id.id)
+                ])
+                _logger.info('[STC] Líneas de plantilla encontradas para el proyecto: %s', template_lines.ids)
+                if template_lines:
+                    order_lines = sale_order.order_line.filtered(lambda l: l.template_line_id in template_lines)
+                    _logger.info('[STC] Líneas de pedido encontradas en el SO: %s', order_lines.ids)
+                    if record.employee_id:
+                        template_lines_with_employee = template_lines.filtered(lambda l: l.employee_id == record.employee_id)
+                        _logger.info('[STC] Líneas de plantilla con empleado %s: %s', record.employee_id.id, template_lines_with_employee.ids)
+                        order_lines_with_employee = order_lines.filtered(lambda l: l.template_line_id in template_lines_with_employee)
+                        _logger.info('[STC] Líneas de pedido con empleado: %s', order_lines_with_employee.ids)
+                        if order_lines_with_employee:
+                            record.so_line = order_lines_with_employee[0].id
+                            _logger.info('[STC] Asociada so_line %s por empleado', order_lines_with_employee[0].id)
+                            return
+                    if order_lines:
+                        record.so_line = order_lines[0].id
+                        _logger.info('[STC] Asociada so_line %s por default', order_lines[0].id)
+                    else:
+                        _logger.info('[STC] No se encontraron líneas de pedido asociables')
+                else:
+                    _logger.info('[STC] No se encontraron líneas de plantilla para el proyecto')
+            else:
+                _logger.info('[STC] Proyecto no tiene reinvoiced_sale_order_id')
+        else:
+            _logger.info('[STC] No tiene proyecto') 
+
+class ProjectProject(models.Model):
+    _inherit = 'project.project'
+
+    sale_order_template_project = fields.Boolean(
+        string='Disponible para plantillas de venta',
+        help='Si está activo, este proyecto puede ser seleccionado en líneas de plantillas de venta.'
+    ) 

+ 10 - 0
sale_template_contract/models/project_project.py

@@ -0,0 +1,10 @@
+from odoo import models, fields
+
+class ProjectProject(models.Model):
+    _inherit = 'project.project'
+ 
+    sale_order_template_project = fields.Boolean(
+        string='Disponible para plantillas de venta',
+        help='Si está activo, este proyecto puede ser seleccionado en líneas de plantillas de venta.',
+        copy=False
+    ) 

+ 1 - 19
sale_template_contract/models/sale_order_line.py

@@ -11,22 +11,4 @@ class SaleOrderLine(models.Model):
         index=True,
         copy=False, # No copiar al duplicar el pedido
         ondelete='set null' # No eliminar la línea si se borra la de la plantilla
-    )
-
-    code = fields.Char(
-        string='Código de Línea',
-        compute='_compute_code',
-        store=True, # Almacenar el valor en la base de datos
-        help="Código único para la línea de pedido, basado en su origen (plantilla o línea de venta directa)."
-    )
-
-    @api.depends('template_line_id', 'product_id', 'product_id.type', 'template_line_id.project_id')
-    def _compute_code(self):
-        for line in self:
-            if line.product_id.type == 'service':
-                if line.template_line_id and line.template_line_id.project_id:
-                    line.code = f"P-{line.product_id.id}"
-                else:
-                    line.code = f"SOL-{line.id}"
-            else:
-                line.code = False # O None, para que el campo quede vacío
+    )

+ 295 - 18
sale_template_contract/models/sale_order_template.py

@@ -1,10 +1,11 @@
 # -*- coding: utf-8 -*-
 from odoo import models, fields, api, _
-from odoo.exceptions import UserError
+from odoo.exceptions import UserError, ValidationError
 from dateutil.relativedelta import relativedelta
 from datetime import datetime, time
 from odoo import Command # Import Command para operaciones de One2many
 import logging
+from collections import defaultdict
 
 _logger = logging.getLogger(__name__)
 
@@ -12,14 +13,20 @@ class SaleOrderTemplate(models.Model):
     _inherit = 'sale.order.template'
 
     use_contract_partner = fields.Boolean(
-        string='Usar Partner de Contrato',
-        help="Marque esta casilla para asociar un Partner específico a esta plantilla y sus líneas."
+        string='Usar la plantilla para creación de órdenes recurrentes',
+        help=""
+    )
+
+    monthly_invoice_project = fields.Boolean(
+        string='Generar ventas y proyectos mensuales',
+        default=True,
+        help=""
     )
 
     contract_partner_id = fields.Many2one(
         'res.partner',
-        string='Partner del Contrato',
-        help="Partner opcional asociado a esta plantilla de presupuesto."
+        string='Cliente',
+        help=""
     )
 
     payment_term_id = fields.Many2one(
@@ -38,6 +45,76 @@ class SaleOrderTemplate(models.Model):
         help="Fecha de finalización para el rango del contrato de esta plantilla."
     )
 
+    @api.constrains('use_contract_partner', 'contract_partner_id', 'payment_term_id', 'date_start', 'date_end')
+    def _check_contract_fields_required(self):
+        for rec in self:
+            if rec.use_contract_partner:
+                missing = []
+                if not rec.contract_partner_id:
+                    missing.append(_('Cliente'))
+                if not rec.payment_term_id:
+                    missing.append(_('Términos de Pago'))
+                if not rec.date_start:
+                    missing.append(_('Fecha de Inicio'))
+                if not rec.date_end:
+                    missing.append(_('Fecha de Fin'))
+                if missing:
+                    raise ValidationError(
+                        _('Los siguientes campos son obligatorios cuando se usa la opción de contrato: %s') % ', '.join(missing)
+                    )
+
+    @api.constrains('sale_order_template_line_ids')
+    def _check_projects_not_used_in_other_templates(self):
+        for rec in self:
+            # Obtener todos los proyectos seleccionados en las líneas de la plantilla actual
+            project_ids = [line.project_id.id for line in rec.sale_order_template_line_ids if line.project_id]
+            if not project_ids:
+                continue
+            # Buscar si alguno de estos proyectos está en otra plantilla
+            conflict_lines = rec.env['sale.order.template.line'].search([
+                ('project_id', 'in', project_ids),
+                ('sale_order_template_id', '!=', rec.id)
+            ])
+            if conflict_lines:
+                conflicts = {}
+                for line in conflict_lines:
+                    conflicts.setdefault(line.project_id.name, set()).add(line.sale_order_template_id.name)
+                msg = _('Los siguientes proyectos ya están usados en otras plantillas:')
+                for project_name, template_names in conflicts.items():
+                    msg += f"\n- {project_name}: {', '.join(template_names)}"
+                raise ValidationError(msg)
+
+    @api.constrains('date_start', 'date_end')
+    def _check_date_start_end(self):
+        for rec in self:
+            if rec.date_start and rec.date_end and rec.date_end <= rec.date_start:
+                raise ValidationError(_('La fecha de fin debe ser mayor a la fecha de inicio.'))
+
+    @api.constrains('sale_order_template_line_ids')
+    def _check_employee_required_for_duplicate_partner_project(self):
+        for rec in self:
+            # Agrupar por (contract_partner_id, project_id)
+            combos = {}
+            for line in rec.sale_order_template_line_ids:
+                key = (line.contract_partner_id.id, line.project_id.id)
+                if not all(key):
+                    continue
+                combos.setdefault(key, []).append(line)
+            # Revisar si hay más de una línea para el mismo combo
+            for key, lines in combos.items():
+                if len(lines) > 1:
+                    employee_ids = set()
+                    for l in lines:
+                        if not l.employee_id:
+                            raise ValidationError(_(
+                                'Si hay más de una línea con el mismo Cliente Final y Proyecto, el campo Empleado es obligatorio en todas esas líneas. (Cliente: %s, Proyecto: %s)'
+                            ) % (l.contract_partner_id.display_name, l.project_id.display_name))
+                        if l.employee_id.id in employee_ids:
+                            raise ValidationError(_(
+                                'No puede haber empleados repetidos en líneas con el mismo Cliente Final y Proyecto. (Cliente: %s, Proyecto: %s, Empleado: %s)'
+                            ) % (l.contract_partner_id.display_name, l.project_id.display_name, l.employee_id.display_name))
+                        employee_ids.add(l.employee_id.id)
+
     def action_generate_contract_orders(self):
         """
         Acción disparada por el botón para generar o actualizar pedidos de contrato.
@@ -52,14 +129,55 @@ class SaleOrderTemplate(models.Model):
                 raise UserError(_("Ocurrió un error al generar los pedidos: %s") % str(e))
         return True # Opcional: puedes devolver una acción de ventana si quieres.
 
+    def _setup_project_mapping(self, order, project_map):
+        """
+        Configura el mapeo de empleados y líneas de pedido en los proyectos.
+        Se aplica tanto para proyectos mensuales como originales.
+        """
+        # Agrupar líneas de plantilla por proyecto
+        project_lines = defaultdict(list)
+        for line in self.sale_order_template_line_ids:
+            if line.project_id:
+                project_lines[line.project_id.id].append(line)
+        for project_id, lines in project_lines.items():
+            if len(lines) > 1:  # Solo si hay más de una línea para el mismo proyecto
+                project = project_map.get(project_id)
+                if not project:
+                    continue
+                for line in lines:
+                    if not line.employee_id:
+                        continue  # Solo mapeos con empleado
+                    # Buscar la línea de pedido correspondiente
+                    so_line = order.order_line.filtered(lambda l: l.template_line_id == line)
+                    if so_line:
+                        mapping = project.sale_line_employee_ids.filtered(lambda m: m.employee_id == line.employee_id)
+                        vals = {
+                            'employee_id': line.employee_id.id,
+                            'sale_line_id': so_line[0].id,
+                            'project_id': project.id,
+                        }
+                        if mapping:
+                            # Si el mapping existe pero no tiene la línea de pedido correcta, actualizar
+                            if not mapping.sale_line_id or mapping.sale_line_id != so_line[0]:
+                                mapping.write({'sale_line_id': so_line[0].id})
+                        else:
+                            project.sale_line_employee_ids = [(0, 0, vals)]
+
     def _get_contract_order_dates(self):
         """
-        Calcula las fechas (primer día de cada mes) para las cuales se deben generar pedidos.
+        Calcula las fechas para las cuales se deben generar pedidos.
+        Si monthly_invoice_project está activo, genera fechas mensuales.
+        Si no, solo genera una orden con fecha actual.
         """
         self.ensure_one()
         if not self.date_start or not self.date_end or self.date_start > self.date_end:
             return []
 
+        if not self.monthly_invoice_project:
+            # Solo crear una orden con fecha actual
+            return [fields.Date.today()]
+
+        # Lógica original para órdenes mensuales
         order_dates = []
         current_date = fields.Date.today()
         
@@ -82,6 +200,118 @@ class SaleOrderTemplate(models.Model):
 
         return order_dates
 
+    def _configure_project_for_contract(self, template_line):
+        """
+        Configura un proyecto para ser billable y asignar el contract_partner_id
+        a la cuenta analítica si existe.
+        """
+        if template_line.contract_partner_id and template_line.project_id:
+            project_updates = {}
+            # Hacer el proyecto billable
+            if not template_line.project_id.allow_billable:
+                project_updates['allow_billable'] = True
+            
+            # Asignar partner del template al proyecto
+            if template_line.project_id.partner_id != self.contract_partner_id:
+                project_updates['partner_id'] = self.contract_partner_id.id
+            
+            # Actualizar proyecto si hay cambios
+            if project_updates:
+                template_line.project_id.write(project_updates)
+
+    def _get_or_create_monthly_projects(self, order_datetime, sale_order=None):
+        """
+        Para cada proyecto único en las líneas del template, crea (o busca) un proyecto mensual
+        para el mes de order_datetime y lo asocia al pedido (sale_order si se provee).
+        Devuelve un mapeo {proyecto_original.id: proyecto_mensual}
+        """
+        project_map = {}
+        unique_projects = {line.project_id for line in self.sale_order_template_line_ids if line.project_id}
+        for project in unique_projects:
+            # Calcular fechas
+            date_start = order_datetime.date()
+            date_end = (order_datetime + relativedelta(months=1, days=-1)).date()
+            # Sumar las horas de todas las líneas de plantilla que usan este proyecto
+            allocated_hours = sum(
+                line.product_uom_qty for line in self.sale_order_template_line_ids
+                if line.project_id and line.project_id.id == project.id and hasattr(line, 'product_uom_qty')
+            )
+            # Buscar si ya existe un proyecto mensual para este pedido y mes
+            domain = [
+                ('name', '=', f"{project.name} - {order_datetime.strftime('%Y-%m')}")
+            ]
+            if sale_order:
+                domain.append(('reinvoiced_sale_order_id', '=', sale_order.id))
+            project_copy = self.env['project.project'].search(domain, limit=1)
+            vals = {
+                'name': f"{project.name} - {order_datetime.strftime('%Y-%m')}",
+                'date_start': date_start,
+                'date': date_end,
+                'allocated_hours': allocated_hours,
+                'sale_line_id': False
+            }
+            # Asignar allow_billable y partner_id si corresponde
+            # Buscar la primera línea de plantilla que use este proyecto y tenga contract_partner_id
+            first_line = next((l for l in self.sale_order_template_line_ids if l.project_id and l.project_id.id == project.id and l.contract_partner_id), None)
+            if first_line:
+                vals['allow_billable'] = True
+                vals['partner_id'] = self.contract_partner_id.id
+            if sale_order:
+                vals['reinvoiced_sale_order_id'] = sale_order.id
+                vals['sale_line_id'] = False
+            if not project_copy:
+                # Copiar el proyecto original
+                project_copy = project.copy(vals)
+                project_copy.write({'sale_line_id': False})
+            else:
+                # Actualizar el proyecto mensual existente con los valores actuales de la plantilla
+                project_copy.write(vals)
+                project_copy.write({'sale_line_id': False})
+            project_map[project.id] = project_copy
+        return project_map
+
+    def _get_project_map(self, order_datetime, sale_order=None):
+        """
+        Obtiene el mapeo de proyectos según el modo de operación.
+        Si monthly_invoice_project está activo, crea proyectos mensuales.
+        Si no, usa los proyectos originales de la plantilla.
+        """
+        if self.monthly_invoice_project:
+            return self._get_or_create_monthly_projects(order_datetime, sale_order)
+        else:
+            # Usar los proyectos originales de la plantilla
+            project_map = {}
+            for line in self.sale_order_template_line_ids:
+                if line.project_id:
+                    project_map[line.project_id.id] = line.project_id
+            # --- Asignar partner de la cuenta analítica solo una vez por proyecto original ---
+            for project in set(project_map.values()):
+                if project.account_id and project.account_id.partner_id != self.contract_partner_id:
+                    project.account_id.write({'partner_id': self.contract_partner_id.id})
+            return project_map
+
+    def _process_order_and_projects(self, order, project_map):
+        """
+        Procesa la orden y sus proyectos: asocia reinvoiced_sale_order_id,
+        configura mapeos de empleados, confirma y factura según corresponda.
+        """
+        # Asociar proyectos al pedido
+        for project in project_map.values():
+            project.write({'reinvoiced_sale_order_id': order.id})
+
+        # Configurar mapeos de empleados
+        self._setup_project_mapping(order, project_map)
+
+        # Confirmar y facturar según el modo
+        if self.monthly_invoice_project:
+            self.create_or_update_downpayment_invoice(order)
+        else:
+            # Solo confirmar la orden sin crear factura
+            if order.state in ['draft', 'sent']:
+                original_date = order.date_order
+                order.action_confirm()
+                order.write({'date_order': original_date})
+
     def _generate_or_update_contract_orders(self):
         self.ensure_one()
         SaleOrder = self.env['sale.order']
@@ -107,6 +337,9 @@ class SaleOrderTemplate(models.Model):
 
             existing_order = SaleOrder.search(domain, limit=1)
 
+            # Obtener mapeo de proyectos según el modo de operación
+            project_map = self._get_project_map(order_datetime, sale_order=existing_order if existing_order else None)
+
             if existing_order:
                 # Nunca tocar pedidos finalizados o cancelados por el usuario.
                 if existing_order.state in ['done', 'cancel']:
@@ -134,7 +367,17 @@ class SaleOrderTemplate(models.Model):
 
                 # 1. Actualizar líneas existentes y encontrar nuevas para crear
                 for t_line_id, t_line in template_lines_map.items():
+                    # Configurar proyecto si es necesario
+                    self._configure_project_for_contract(t_line)
+                    
                     vals = t_line._prepare_order_line_values()
+                    # Si monthly_invoice_project está activo y la línea tiene proyecto, asignar el proyecto mensual
+                    if self.monthly_invoice_project and t_line.project_id and t_line.project_id.id in project_map:
+                        project_month = project_map[t_line.project_id.id]
+                        vals['project_id'] = project_month.id
+                        # Asignar la cuenta analítica del proyecto mensual a la distribución analítica
+                        if project_month.account_id:
+                            vals['analytic_distribution'] = {project_month.account_id.id: 100}
                     if t_line_id in order_lines_map:
                         o_line = order_lines_map[t_line_id]
                         # Comparar campos clave para ver si se necesita una actualización.
@@ -161,19 +404,35 @@ class SaleOrderTemplate(models.Model):
                             new_sol._compute_price_unit()
                             target_price_unit = new_sol.price_unit
 
-                        # También actualizar si cambia el project_id
-                        if (o_line.product_uom_qty != qty_in_template or
-                           (name_in_template and o_line.name != name_in_template) or
-                           o_line.price_unit != target_price_unit or
-                           o_line.project_id != t_line.project_id):
-
+                        # Verificar si hay cambios que requieran actualización
+                        needs_update = (
+                            o_line.product_uom_qty != qty_in_template or
+                            (name_in_template and o_line.name != name_in_template) or
+                            o_line.price_unit != target_price_unit or
+                            o_line.project_id != t_line.project_id
+                        )
+                        
+                        # Verificar cambios en distribución analítica
+                        analytic_distribution_in_template = vals.get('analytic_distribution', {})
+                        if o_line.analytic_distribution != analytic_distribution_in_template:
+                            needs_update = True
+
+                        if needs_update:
                             update_payload = {
                                 'product_uom_qty': qty_in_template,
                                 'price_unit': target_price_unit,
+                                'analytic_distribution': analytic_distribution_in_template,
                             }
                             if name_in_template:
                                 update_payload['name'] = name_in_template
-                            if o_line.project_id != t_line.project_id:
+                            # Si monthly_invoice_project está activo y la línea tiene proyecto, asignar el proyecto mensual
+                            if self.monthly_invoice_project and t_line.project_id and t_line.project_id.id in project_map:
+                                project_month = project_map[t_line.project_id.id]
+                                update_payload['project_id'] = project_month.id
+                                # Asignar la cuenta analítica del proyecto mensual a la distribución analítica
+                                if project_month.account_id:
+                                    update_payload['analytic_distribution'] = {project_month.account_id.id: 100}
+                            elif o_line.project_id != t_line.project_id:
                                 update_payload['project_id'] = t_line.project_id.id if t_line.project_id else False
 
                             commands.append(Command.update(o_line.id, update_payload))
@@ -193,8 +452,6 @@ class SaleOrderTemplate(models.Model):
                 # 4. Escribir todos los cambios de líneas en una sola operación
                 if commands:
                     existing_order.write({'order_line': commands})
-                    # Forzar recálculo del campo 'code' en las líneas de pedido actualizadas
-                    existing_order.order_line._fields['code'].recompute(existing_order.order_line)
 
                 # 5. Actualizar campos del pedido y la fecha
                 update_vals = {'date_order': order_datetime}
@@ -202,13 +459,31 @@ class SaleOrderTemplate(models.Model):
                     update_vals['payment_term_id'] = self.payment_term_id.id
                 existing_order.write(update_vals)
 
-                self.create_or_update_downpayment_invoice(existing_order)
+                # Procesar orden y proyectos
+                self._process_order_and_projects(existing_order, project_map)
             else:
                 # Crear nuevo pedido
                 pricelist = self.contract_partner_id.property_product_pricelist
                 # Priorizar término de pago de la plantilla, si no, el del partner
                 payment_term = self.payment_term_id or self.contract_partner_id.property_payment_term_id
-                order_lines_vals = [line._prepare_order_line_values() for line in self.sale_order_template_line_ids]
+                
+                # Configurar proyectos antes de crear líneas
+                for line in self.sale_order_template_line_ids:
+                    self._configure_project_for_contract(line)
+                
+                # Obtener mapeo de proyectos
+                project_map = self._get_project_map(order_datetime)
+                
+                order_lines_vals = []
+                for line in self.sale_order_template_line_ids:
+                    vals = line._prepare_order_line_values()
+                    if self.monthly_invoice_project and line.project_id and line.project_id.id in project_map:
+                        project_month = project_map[line.project_id.id]
+                        vals['project_id'] = project_month.id
+                        # Asignar la cuenta analítica del proyecto mensual a la distribución analítica
+                        if project_month.account_id:
+                            vals['analytic_distribution'] = {project_month.account_id.id: 100}
+                    order_lines_vals.append(vals)
 
                 order_vals = {
                     'partner_id': self.contract_partner_id.id,
@@ -220,7 +495,9 @@ class SaleOrderTemplate(models.Model):
                     'order_line': [Command.create(vals) for vals in order_lines_vals]
                 }
                 new_order = SaleOrder.create(order_vals)
-                self.create_or_update_downpayment_invoice(new_order)
+                
+                # Procesar orden y proyectos
+                self._process_order_and_projects(new_order, project_map)
 
     def create_or_update_downpayment_invoice(self, order):
         """

+ 35 - 8
sale_template_contract/models/sale_order_template_line.py

@@ -1,30 +1,54 @@
 # -*- coding: utf-8 -*-
-from odoo import models, fields
+from odoo import models, fields, api
 
 class SaleOrderTemplateLine(models.Model):
     _inherit = 'sale.order.template.line'
 
+    # Este campo es crucial para que los dominios funcionen correctamente.
+    # Al estar presente, Odoo puede filtrar automáticamente los campos
+    # relacionados por compañía.
+    company_id = fields.Many2one(
+        'res.company',
+        related='sale_order_template_id.company_id',
+        store=True,
+        readonly=True,
+        index=True
+    )
+
+    # Heredamos el campo nativo para asegurar que el dominio se aplique.
+    # El dominio permite productos globales (sin compañía) o de la compañía de la plantilla.
+    # Mantenemos ('sale_ok', '=', True) para mostrar solo productos vendibles.
+    product_id = fields.Many2one(
+        domain="[('sale_ok', '=', True), '|', ('company_id', '=', False), ('company_id', '=', company_id)]"
+    )
+
+    # Este dominio es más flexible, permitiendo clientes globales o de la misma compañía.
     contract_partner_id = fields.Many2one(
         'res.partner',
-        string='Partner del Contrato (Línea)',
-        help="Partner opcional asociado a esta línea de la plantilla de presupuesto."
+        string='Cliente final',
+        help="Partner asociado a esta línea de la plantilla de presupuesto.",
+        domain="['|', ('company_id', '=', False), ('company_id', '=', company_id)]"
     )
 
+    # Los empleados y proyectos suelen ser específicos de una compañía.
     employee_id = fields.Many2one(
         'hr.employee',
         string='Empleado',
-        help="Empleado opcional asociado a esta línea de la plantilla de presupuesto."
+        help="Empleado opcional asociado a esta línea de la plantilla de presupuesto.",
+        domain="[('company_id', '=', company_id)]"
     )
 
     price_unit = fields.Float(
-        string='Precio Unitario (Plantilla)',
-        help="Precio unitario opcional para esta línea de la plantilla. Si se deja en cero, se usará el precio del producto."
+        string='Precio Unitario',
+        help="Precio unitario para esta línea de la plantilla. Si se deja en cero, se usará el precio del producto."
     )
-
+ 
+    # Los empleados y proyectos suelen ser específicos de una compañía.
     project_id = fields.Many2one(
         'project.project',
         string='Proyecto',
-        help="Proyecto opcional asociado a esta línea de la plantilla de presupuesto."
+        help="Proyecto plantilla asociado a esta línea de la plantilla de presupuesto.",
+        domain="[('company_id', '=', company_id)]"
     )
 
     def _prepare_order_line_values(self):
@@ -48,4 +72,7 @@ class SaleOrderTemplateLine(models.Model):
                 vals['name'] = f"{vals['name']}\n{extra_desc}"
             else:
                 vals['name'] = extra_desc
+        # --- NUEVO: asignar distribución analítica si aplica ---
+        if self.project_id and self.project_id.account_id:
+            vals['analytic_distribution'] = {self.project_id.account_id.id: 100}
         return vals

+ 14 - 0
sale_template_contract/views/project_project_views.xml

@@ -0,0 +1,14 @@
+<odoo>
+    <data>
+        <record id="view_project_form_inherit_sale_template_contract" model="ir.ui.view">
+            <field name="name">project.project.form.inherit.sale.template.contract</field>
+            <field name="model">project.project</field>
+            <field name="inherit_id" ref="project.edit_project"/>
+            <field name="arch" type="xml">
+                <xpath expr="//page[@name='settings']/group/group" position="inside">
+                    <field name="sale_order_template_project"/>
+                </xpath>
+            </field>
+        </record>
+    </data>
+</odoo> 

+ 13 - 11
sale_template_contract/views/sale_order_template_views.xml

@@ -12,37 +12,39 @@
                             type="object"
                             string="Generar/Actualizar Pedidos de Contrato"
                             class="oe_highlight"
-                            invisible="not use_contract_partner or not contract_partner_id or not date_start or not date_end"/>
+                            invisible="not use_contract_partner or not contract_partner_id or not date_start or not date_end or not sale_order_template_line_ids"/>
                 </xpath>
 
                 <xpath expr="//group[@name='so_confirmation']" position="inside">
                     <field name="use_contract_partner"/>
                     <field name="contract_partner_id"
                            options="{'no_create': True, 'no_open': False}"
+                           required="use_contract_partner"
                            invisible="not use_contract_partner"/>
-                    <field name="payment_term_id" invisible="not use_contract_partner"/>
-                    <field name="date_start" invisible="not use_contract_partner"/>
-                    <field name="date_end" invisible="not use_contract_partner"/>
+                    <field name="monthly_invoice_project" invisible="not use_contract_partner" required="use_contract_partner"/>
+                    <field name="payment_term_id" invisible="not use_contract_partner" required="use_contract_partner"/>
+                    <field name="date_start" invisible="not use_contract_partner" required="use_contract_partner"/>
+                    <field name="date_end" invisible="not use_contract_partner" required="use_contract_partner"/>
                 </xpath>
 
                 <xpath expr="//field[@name='sale_order_template_line_ids']/form/group/group/field[@name='product_id']" position="after">
                     <field name="contract_partner_id" optional="show" options="{'no_create': True, 'no_open': False}"
-                           column_invisible="not parent.use_contract_partner"/>
+                           column_invisible="not parent.use_contract_partner" required="parent.use_contract_partner" />
                     <field name="employee_id" optional="show" options="{'no_create': True, 'no_open': False}"
                            column_invisible="not parent.use_contract_partner"/>
                     <field name="price_unit" optional="show" column_invisible="not parent.use_contract_partner"/>
-                    <field name="project_id" optional="show" options="{'no_create': False, 'no_open': False}"
-                           column_invisible="not parent.use_contract_partner"/>
+                    <field name="project_id" optional="show" options="{'no_create': True, 'no_open': False}"
+                           column_invisible="not parent.use_contract_partner" required="parent.use_contract_partner"/>
                 </xpath>
                 
                 <xpath expr="//field[@name='sale_order_template_line_ids']/list/field[@name='product_id']" position="after">
                     <field name="contract_partner_id" optional="show" options="{'no_create': True, 'no_open': False}"
-                           column_invisible="not parent.use_contract_partner"/>
+                           column_invisible="not parent.use_contract_partner" required="parent.use_contract_partner"/>
                     <field name="employee_id" optional="show" options="{'no_create': True, 'no_open': False}"
                            column_invisible="not parent.use_contract_partner"/>
-                    <field name="price_unit" optional="show" column_invisible="not parent.use_contract_partner"/>
-                    <field name="project_id" optional="show" options="{'no_create': False, 'no_open': False}"
-                           column_invisible="not parent.use_contract_partner"/>
+                    <field name="price_unit" optional="show" column_invisible="not parent.use_contract_partner" required="parent.use_contract_partner"/>
+                    <field name="project_id" optional="show" options="{'no_create': True, 'no_open': False}"
+                           column_invisible="not parent.use_contract_partner" required="parent.use_contract_partner"/>
                 </xpath>
             </field>
         </record>