|
@@ -1,10 +1,11 @@
|
|
|
# -*- coding: utf-8 -*-
|
|
# -*- coding: utf-8 -*-
|
|
|
from odoo import models, fields, api, _
|
|
from odoo import models, fields, api, _
|
|
|
-from odoo.exceptions import UserError
|
|
|
|
|
|
|
+from odoo.exceptions import UserError, ValidationError
|
|
|
from dateutil.relativedelta import relativedelta
|
|
from dateutil.relativedelta import relativedelta
|
|
|
from datetime import datetime, time
|
|
from datetime import datetime, time
|
|
|
from odoo import Command # Import Command para operaciones de One2many
|
|
from odoo import Command # Import Command para operaciones de One2many
|
|
|
import logging
|
|
import logging
|
|
|
|
|
+from collections import defaultdict
|
|
|
|
|
|
|
|
_logger = logging.getLogger(__name__)
|
|
_logger = logging.getLogger(__name__)
|
|
|
|
|
|
|
@@ -12,14 +13,20 @@ class SaleOrderTemplate(models.Model):
|
|
|
_inherit = 'sale.order.template'
|
|
_inherit = 'sale.order.template'
|
|
|
|
|
|
|
|
use_contract_partner = fields.Boolean(
|
|
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(
|
|
contract_partner_id = fields.Many2one(
|
|
|
'res.partner',
|
|
'res.partner',
|
|
|
- string='Partner del Contrato',
|
|
|
|
|
- help="Partner opcional asociado a esta plantilla de presupuesto."
|
|
|
|
|
|
|
+ string='Cliente',
|
|
|
|
|
+ help=""
|
|
|
)
|
|
)
|
|
|
|
|
|
|
|
payment_term_id = fields.Many2one(
|
|
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."
|
|
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):
|
|
def action_generate_contract_orders(self):
|
|
|
"""
|
|
"""
|
|
|
Acción disparada por el botón para generar o actualizar pedidos de contrato.
|
|
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))
|
|
raise UserError(_("Ocurrió un error al generar los pedidos: %s") % str(e))
|
|
|
return True # Opcional: puedes devolver una acción de ventana si quieres.
|
|
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):
|
|
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()
|
|
self.ensure_one()
|
|
|
if not self.date_start or not self.date_end or self.date_start > self.date_end:
|
|
if not self.date_start or not self.date_end or self.date_start > self.date_end:
|
|
|
return []
|
|
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 = []
|
|
order_dates = []
|
|
|
current_date = fields.Date.today()
|
|
current_date = fields.Date.today()
|
|
|
|
|
|
|
@@ -82,6 +200,118 @@ class SaleOrderTemplate(models.Model):
|
|
|
|
|
|
|
|
return order_dates
|
|
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):
|
|
def _generate_or_update_contract_orders(self):
|
|
|
self.ensure_one()
|
|
self.ensure_one()
|
|
|
SaleOrder = self.env['sale.order']
|
|
SaleOrder = self.env['sale.order']
|
|
@@ -107,6 +337,9 @@ class SaleOrderTemplate(models.Model):
|
|
|
|
|
|
|
|
existing_order = SaleOrder.search(domain, limit=1)
|
|
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:
|
|
if existing_order:
|
|
|
# Nunca tocar pedidos finalizados o cancelados por el usuario.
|
|
# Nunca tocar pedidos finalizados o cancelados por el usuario.
|
|
|
if existing_order.state in ['done', 'cancel']:
|
|
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
|
|
# 1. Actualizar líneas existentes y encontrar nuevas para crear
|
|
|
for t_line_id, t_line in template_lines_map.items():
|
|
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()
|
|
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:
|
|
if t_line_id in order_lines_map:
|
|
|
o_line = order_lines_map[t_line_id]
|
|
o_line = order_lines_map[t_line_id]
|
|
|
# Comparar campos clave para ver si se necesita una actualización.
|
|
# Comparar campos clave para ver si se necesita una actualización.
|
|
@@ -161,19 +404,35 @@ class SaleOrderTemplate(models.Model):
|
|
|
new_sol._compute_price_unit()
|
|
new_sol._compute_price_unit()
|
|
|
target_price_unit = new_sol.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 = {
|
|
update_payload = {
|
|
|
'product_uom_qty': qty_in_template,
|
|
'product_uom_qty': qty_in_template,
|
|
|
'price_unit': target_price_unit,
|
|
'price_unit': target_price_unit,
|
|
|
|
|
+ 'analytic_distribution': analytic_distribution_in_template,
|
|
|
}
|
|
}
|
|
|
if name_in_template:
|
|
if name_in_template:
|
|
|
update_payload['name'] = 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
|
|
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))
|
|
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
|
|
# 4. Escribir todos los cambios de líneas en una sola operación
|
|
|
if commands:
|
|
if commands:
|
|
|
existing_order.write({'order_line': 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
|
|
# 5. Actualizar campos del pedido y la fecha
|
|
|
update_vals = {'date_order': order_datetime}
|
|
update_vals = {'date_order': order_datetime}
|
|
@@ -202,13 +459,31 @@ class SaleOrderTemplate(models.Model):
|
|
|
update_vals['payment_term_id'] = self.payment_term_id.id
|
|
update_vals['payment_term_id'] = self.payment_term_id.id
|
|
|
existing_order.write(update_vals)
|
|
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:
|
|
else:
|
|
|
# Crear nuevo pedido
|
|
# Crear nuevo pedido
|
|
|
pricelist = self.contract_partner_id.property_product_pricelist
|
|
pricelist = self.contract_partner_id.property_product_pricelist
|
|
|
# Priorizar término de pago de la plantilla, si no, el del partner
|
|
# 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
|
|
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 = {
|
|
order_vals = {
|
|
|
'partner_id': self.contract_partner_id.id,
|
|
'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]
|
|
'order_line': [Command.create(vals) for vals in order_lines_vals]
|
|
|
}
|
|
}
|
|
|
new_order = SaleOrder.create(order_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):
|
|
def create_or_update_downpayment_invoice(self, order):
|
|
|
"""
|
|
"""
|