sale_order_template.py 34 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656
  1. # -*- coding: utf-8 -*-
  2. from odoo import models, fields, api, _
  3. from odoo.exceptions import UserError, ValidationError
  4. from dateutil.relativedelta import relativedelta
  5. from datetime import datetime, time
  6. from odoo import Command # Import Command para operaciones de One2many
  7. import logging
  8. from collections import defaultdict
  9. _logger = logging.getLogger(__name__)
  10. class SaleOrderTemplate(models.Model):
  11. _inherit = 'sale.order.template'
  12. use_contract_partner = fields.Boolean(
  13. string='Creación de órdenes y proyectos',
  14. help=""
  15. )
  16. monthly_invoice_project = fields.Boolean(
  17. string='Generar órdenes y proyectos mensuales',
  18. default=True,
  19. help="No puede cambiarse si ya hay órdenes generadas"
  20. )
  21. contract_partner_id = fields.Many2one(
  22. 'res.partner',
  23. string='Cliente',
  24. help=""
  25. )
  26. payment_term_id = fields.Many2one(
  27. 'account.payment.term',
  28. string='Términos de Pago',
  29. company_dependent=True,
  30. help="Términos de pago que se aplicarán a los pedidos generados desde esta plantilla."
  31. )
  32. opportunity_id = fields.Many2one(
  33. 'crm.lead',
  34. string='Oportunidad',
  35. domain="[('type', '=', 'opportunity')]",
  36. help="Oportunidad opcional que se asignará a los pedidos generados desde esta plantilla."
  37. )
  38. date_start = fields.Date(
  39. string='Fecha de Inicio del Contrato',
  40. help="Fecha de inicio para el rango del contrato de esta plantilla."
  41. )
  42. date_end = fields.Date(
  43. string='Fecha de Fin del Contrato',
  44. help="Fecha de finalización para el rango del contrato de esta plantilla."
  45. )
  46. has_contract_orders = fields.Boolean(
  47. string='Tiene órdenes de contrato',
  48. compute='_compute_has_contract_orders',
  49. store=False
  50. )
  51. def _compute_has_contract_orders(self):
  52. SaleOrder = self.env['sale.order']
  53. for rec in self:
  54. count = SaleOrder.search_count([('sale_order_template_id', '=', rec.id)])
  55. rec.has_contract_orders = count > 0
  56. @api.constrains('use_contract_partner', 'contract_partner_id', 'payment_term_id', 'date_start', 'date_end')
  57. def _check_contract_fields_required(self):
  58. for rec in self:
  59. if rec.use_contract_partner:
  60. missing = []
  61. if not rec.contract_partner_id:
  62. missing.append(_('Cliente'))
  63. if not rec.payment_term_id:
  64. missing.append(_('Términos de Pago'))
  65. if not rec.date_start:
  66. missing.append(_('Fecha de Inicio'))
  67. if not rec.date_end:
  68. missing.append(_('Fecha de Fin'))
  69. if missing:
  70. raise ValidationError(
  71. _('Los siguientes campos son obligatorios cuando se usa la opción de contrato: %s') % ', '.join(missing)
  72. )
  73. @api.constrains('sale_order_template_line_ids')
  74. def _check_projects_not_used_in_other_templates(self):
  75. for rec in self:
  76. # Obtener todos los proyectos seleccionados en las líneas de la plantilla actual
  77. project_ids = [line.project_id.id for line in rec.sale_order_template_line_ids if line.project_id]
  78. if not project_ids:
  79. continue
  80. # Buscar si alguno de estos proyectos está en otra plantilla
  81. conflict_lines = rec.env['sale.order.template.line'].search([
  82. ('project_id', 'in', project_ids),
  83. ('sale_order_template_id', '!=', rec.id)
  84. ])
  85. if conflict_lines:
  86. conflicts = {}
  87. for line in conflict_lines:
  88. conflicts.setdefault(line.project_id.name, set()).add(line.sale_order_template_id.name)
  89. msg = _('Los siguientes proyectos ya están usados en otras plantillas:')
  90. for project_name, template_names in conflicts.items():
  91. msg += f"\n- {project_name}: {', '.join(template_names)}"
  92. raise ValidationError(msg)
  93. @api.constrains('date_start', 'date_end')
  94. def _check_date_start_end(self):
  95. for rec in self:
  96. if rec.date_start and rec.date_end and rec.date_end <= rec.date_start:
  97. raise ValidationError(_('La fecha de fin debe ser mayor a la fecha de inicio.'))
  98. @api.constrains('company_id', 'sale_order_template_line_ids', 'sale_order_template_option_ids')
  99. def _check_company_id(self):
  100. """
  101. Sobrescribe la validación del modelo padre para deshabilitarla cuando use_contract_partner está habilitado.
  102. """
  103. # Filtrar solo los registros que NO tienen use_contract_partner habilitado
  104. templates_to_validate = self.filtered(lambda t: not t.use_contract_partner)
  105. # Llamar al método padre solo para los registros que necesitan validación
  106. if templates_to_validate:
  107. super(SaleOrderTemplate, templates_to_validate)._check_company_id()
  108. @api.constrains('sale_order_template_line_ids')
  109. def _check_employee_required_for_duplicate_partner_project(self):
  110. for rec in self:
  111. # Agrupar por (contract_partner_id, project_id)
  112. combos = {}
  113. for line in rec.sale_order_template_line_ids:
  114. key = (line.contract_partner_id.id, line.project_id.id)
  115. if not all(key):
  116. continue
  117. combos.setdefault(key, []).append(line)
  118. # Revisar si hay más de una línea para el mismo combo
  119. for key, lines in combos.items():
  120. if len(lines) > 1:
  121. employee_ids = set()
  122. for l in lines:
  123. if not l.employee_id:
  124. raise ValidationError(_(
  125. '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)'
  126. ) % (l.contract_partner_id.display_name, l.project_id.display_name))
  127. if l.employee_id.id in employee_ids:
  128. raise ValidationError(_(
  129. 'No puede haber empleados repetidos en líneas con el mismo Cliente Final y Proyecto. (Cliente: %s, Proyecto: %s, Empleado: %s)'
  130. ) % (l.contract_partner_id.display_name, l.project_id.display_name, l.employee_id.display_name))
  131. employee_ids.add(l.employee_id.id)
  132. def action_generate_contract_orders(self):
  133. """
  134. Acción disparada por el botón para generar o actualizar pedidos de contrato.
  135. """
  136. for template in self:
  137. if not template.use_contract_partner or not template.contract_partner_id or \
  138. not template.date_start or not template.date_end:
  139. raise UserError(_("Por favor, asegúrese de que 'Usar Partner de Contrato' esté marcado y que el partner, la fecha de inicio y la fecha de fin del contrato estén establecidos."))
  140. try:
  141. template._generate_or_update_contract_orders()
  142. except Exception as e:
  143. raise UserError(_("Ocurrió un error al generar los pedidos: %s") % str(e))
  144. return True # Opcional: puedes devolver una acción de ventana si quieres.
  145. def _setup_project_mapping(self, order, project_map):
  146. """
  147. Configura el mapeo de empleados y líneas de pedido en los proyectos.
  148. Se aplica tanto para proyectos mensuales como originales.
  149. """
  150. # Agrupar líneas de plantilla por proyecto
  151. project_lines = defaultdict(list)
  152. for line in self.sale_order_template_line_ids:
  153. if line.project_id:
  154. project_lines[line.project_id.id].append(line)
  155. for project_id, lines in project_lines.items():
  156. if len(lines) > 1: # Solo si hay más de una línea para el mismo proyecto
  157. project = project_map.get(project_id)
  158. if not project:
  159. continue
  160. for line in lines:
  161. if not line.employee_id:
  162. continue # Solo mapeos con empleado
  163. # Buscar la línea de pedido correspondiente
  164. so_line = order.order_line.filtered(lambda l: l.template_line_id == line)
  165. if so_line:
  166. mapping = project.sale_line_employee_ids.filtered(lambda m: m.employee_id == line.employee_id)
  167. vals = {
  168. 'employee_id': line.employee_id.id,
  169. 'sale_line_id': so_line[0].id,
  170. 'project_id': project.id,
  171. }
  172. if mapping:
  173. # Si el mapping existe pero no tiene la línea de pedido correcta, actualizar
  174. if not mapping.sale_line_id or mapping.sale_line_id != so_line[0]:
  175. mapping.write({'sale_line_id': so_line[0].id})
  176. else:
  177. project.sale_line_employee_ids = [(0, 0, vals)]
  178. def _get_contract_order_dates(self):
  179. """
  180. Calcula las fechas para las cuales se deben generar pedidos.
  181. Si monthly_invoice_project está activo, genera fechas mensuales.
  182. Si no, solo genera una orden con la fecha de inicio del contrato.
  183. """
  184. self.ensure_one()
  185. if not self.date_start or not self.date_end or self.date_start > self.date_end:
  186. return []
  187. if not self.monthly_invoice_project:
  188. # Solo crear una orden con la fecha de inicio del contrato
  189. return [self.date_start]
  190. # Lógica original para órdenes mensuales
  191. order_dates = []
  192. current_date = fields.Date.today()
  193. actual_start_date = self.date_start.replace(day=1)
  194. if actual_start_date > self.date_end:
  195. return []
  196. ptr_date = actual_start_date
  197. while ptr_date <= self.date_end:
  198. order_dates.append(ptr_date)
  199. ptr_date += relativedelta(months=1)
  200. if ptr_date.day != 1:
  201. ptr_date = ptr_date.replace(day=1)
  202. return order_dates
  203. def _update_analytic_account_partner(self, original_account, partner_id):
  204. """
  205. Actualiza el partner de una cuenta analítica existente.
  206. Si la cuenta analítica ya tiene el partner correcto, no hace nada.
  207. """
  208. if not original_account:
  209. return original_account
  210. # Si la cuenta analítica ya tiene el partner correcto, no hacer nada
  211. if original_account.partner_id.id == partner_id:
  212. return original_account
  213. # Actualizar el partner de la cuenta analítica existente
  214. original_account.write({'partner_id': partner_id})
  215. return original_account
  216. def _configure_project_for_contract(self, template_line):
  217. """
  218. Configura un proyecto para ser billable y asignar el contract_partner_id
  219. a la cuenta analítica si existe.
  220. """
  221. if template_line.contract_partner_id and template_line.project_id:
  222. project_updates = {}
  223. # Hacer el proyecto billable
  224. if not template_line.project_id.allow_billable:
  225. project_updates['allow_billable'] = True
  226. # Asignar partner del template al proyecto
  227. if template_line.project_id.partner_id != self.contract_partner_id:
  228. project_updates['partner_id'] = self.contract_partner_id.id
  229. # Asignar partner de la cuenta analítica del proyecto
  230. if template_line.project_id.account_id and template_line.project_id.account_id.partner_id != template_line.contract_partner_id:
  231. project_updates['account_id'] = template_line.project_id.account_id.id
  232. # Actualizar el partner de la cuenta analítica
  233. template_line.project_id.account_id = self._update_analytic_account_partner(template_line.project_id.account_id, template_line.contract_partner_id.id)
  234. # Actualizar proyecto si hay cambios
  235. if project_updates:
  236. template_line.project_id.write(project_updates)
  237. def _get_or_create_monthly_projects(self, order_datetime, sale_order=None):
  238. """
  239. Para cada proyecto único en las líneas del template, crea (o busca) un proyecto mensual
  240. para el mes de order_datetime y lo asocia al pedido (sale_order si se provee).
  241. Devuelve un mapeo {proyecto_original.id: proyecto_mensual}
  242. """
  243. project_map = {}
  244. unique_projects = {line.project_id for line in self.sale_order_template_line_ids if line.project_id}
  245. for project in unique_projects:
  246. # Calcular fechas
  247. date_start = order_datetime.date()
  248. date_end = (order_datetime + relativedelta(months=1, days=-1)).date()
  249. # Sumar las horas de todas las líneas de plantilla que usan este proyecto
  250. allocated_hours = sum(
  251. line.product_uom_qty for line in self.sale_order_template_line_ids
  252. if line.project_id and line.project_id.id == project.id and hasattr(line, 'product_uom_qty')
  253. )
  254. # Determinar el nombre del proyecto
  255. project_name = project.name
  256. if self.opportunity_id and self.opportunity_id.name:
  257. # Si hay oportunidad, usar su nombre para el proyecto
  258. project_name = self.opportunity_id.name
  259. # Buscar si ya existe un proyecto mensual para este pedido y mes
  260. domain = [
  261. ('name', '=', f"{project_name} - {order_datetime.strftime('%Y-%m')}")
  262. ]
  263. if sale_order:
  264. domain.append(('reinvoiced_sale_order_id', '=', sale_order.id))
  265. project_copy = self.env['project.project'].search(domain, limit=1)
  266. vals = {
  267. 'name': f"{project_name} - {order_datetime.strftime('%Y-%m')}",
  268. 'date_start': date_start,
  269. 'date': date_end,
  270. 'allocated_hours': allocated_hours,
  271. 'sale_line_id': False
  272. }
  273. # Asignar allow_billable y partner_id si corresponde
  274. # Buscar la primera línea de plantilla que use este proyecto y tenga contract_partner_id
  275. 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)
  276. if first_line:
  277. vals['allow_billable'] = True
  278. # Usar siempre el contract_partner_id para proyectos
  279. vals['partner_id'] = self.contract_partner_id.id
  280. # No asignar account_id aquí, se generará automáticamente al crear el proyecto
  281. if sale_order:
  282. vals['reinvoiced_sale_order_id'] = sale_order.id
  283. vals['sale_line_id'] = False
  284. if not project_copy:
  285. # Copiar el proyecto original
  286. project_copy = project.copy(vals)
  287. project_copy.write({'sale_line_id': False})
  288. # Actualizar el partner de la cuenta analítica después de crear el proyecto
  289. if project_copy.account_id and first_line:
  290. # Usar siempre el contract_partner_id para cuentas analíticas
  291. self._update_analytic_account_partner(project_copy.account_id, first_line.contract_partner_id.id)
  292. else:
  293. # Actualizar el proyecto mensual existente con los valores actuales de la plantilla
  294. project_copy.write(vals)
  295. project_copy.write({'sale_line_id': False})
  296. # Actualizar el partner de la cuenta analítica
  297. if project_copy.account_id and first_line:
  298. # Usar siempre el contract_partner_id para cuentas analíticas
  299. self._update_analytic_account_partner(project_copy.account_id, first_line.contract_partner_id.id)
  300. project_map[project.id] = project_copy
  301. return project_map
  302. def _get_project_map(self, order_datetime, sale_order=None):
  303. """
  304. Obtiene el mapeo de proyectos según el modo de operación.
  305. Si monthly_invoice_project está activo, crea proyectos mensuales.
  306. Si no, crea una copia del proyecto original para el pedido (con el mismo nombre y fechas del contrato).
  307. """
  308. if self.monthly_invoice_project:
  309. return self._get_or_create_monthly_projects(order_datetime, sale_order)
  310. else:
  311. # Crear una copia del proyecto original para el pedido (con el mismo nombre y fechas del contrato)
  312. project_map = {}
  313. unique_projects = {line.project_id for line in self.sale_order_template_line_ids if line.project_id}
  314. for project in unique_projects:
  315. # Determinar el nombre del proyecto
  316. project_name = project.name
  317. if self.opportunity_id and self.opportunity_id.name:
  318. # Si hay oportunidad, usar su nombre para el proyecto
  319. project_name = self.opportunity_id.name
  320. # Buscar si ya existe una copia para este pedido, nombre y fechas
  321. domain = [
  322. ('name', '=', project_name),
  323. ('date_start', '=', self.date_start),
  324. ('date', '=', self.date_end),
  325. ]
  326. if sale_order:
  327. domain.append(('reinvoiced_sale_order_id', '=', sale_order.id))
  328. project_copy = self.env['project.project'].search(domain, limit=1)
  329. vals = {
  330. 'name': project_name,
  331. 'date_start': self.date_start,
  332. 'date': self.date_end,
  333. 'sale_line_id': False
  334. }
  335. # Asignar allow_billable y partner_id si corresponde
  336. 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)
  337. if first_line:
  338. vals['allow_billable'] = True
  339. # Usar siempre el contract_partner_id para proyectos
  340. vals['partner_id'] = self.contract_partner_id.id
  341. # No asignar account_id aquí, se generará automáticamente al crear el proyecto
  342. if sale_order:
  343. vals['reinvoiced_sale_order_id'] = sale_order.id
  344. vals['sale_line_id'] = False
  345. if not project_copy:
  346. project_copy = project.copy(vals)
  347. project_copy.write({'sale_line_id': False})
  348. # Actualizar el partner de la cuenta analítica después de crear el proyecto
  349. if project_copy.account_id and first_line:
  350. # Usar siempre el contract_partner_id para cuentas analíticas
  351. self._update_analytic_account_partner(project_copy.account_id, first_line.contract_partner_id.id)
  352. else:
  353. project_copy.write(vals)
  354. project_copy.write({'sale_line_id': False})
  355. # Actualizar el partner de la cuenta analítica
  356. if project_copy.account_id and first_line:
  357. # Usar siempre el contract_partner_id para cuentas analíticas
  358. self._update_analytic_account_partner(project_copy.account_id, first_line.contract_partner_id.id)
  359. project_map[project.id] = project_copy
  360. return project_map
  361. def _process_order_and_projects(self, order, project_map):
  362. """
  363. Procesa la orden y sus proyectos: asocia reinvoiced_sale_order_id,
  364. configura mapeos de empleados, confirma y factura según corresponda.
  365. """
  366. # Asociar proyectos al pedido
  367. for project in project_map.values():
  368. project.write({'reinvoiced_sale_order_id': order.id})
  369. # Configurar mapeos de empleados
  370. self._setup_project_mapping(order, project_map)
  371. # Confirmar y facturar según el modo
  372. if self.monthly_invoice_project:
  373. self.create_or_update_downpayment_invoice(order)
  374. else:
  375. # Solo confirmar la orden sin crear factura
  376. if order.state in ['draft', 'sent']:
  377. original_date = order.date_order
  378. order.action_confirm()
  379. order.write({'date_order': original_date})
  380. def _generate_or_update_contract_orders(self):
  381. self.ensure_one()
  382. SaleOrder = self.env['sale.order']
  383. if not self.use_contract_partner or not self.contract_partner_id or not self.date_start or not self.date_end:
  384. # No hacer nada si no están las condiciones
  385. return
  386. order_dates = self._get_contract_order_dates()
  387. for order_date in order_dates:
  388. # Convertir a datetime a las 12:00 para evitar desfases de zona horaria
  389. order_datetime = datetime.combine(order_date, time(12, 0, 0))
  390. # Buscar un pedido existente para este partner, plantilla y mes/año.
  391. # Usar sale_order_template_id es clave para identificar unívocamente el pedido
  392. # y evitar duplicados, sin importar el estado del pedido.
  393. # Usar siempre el contract_partner_id para búsqueda de pedidos existentes
  394. domain = [
  395. ('partner_id', '=', self.contract_partner_id.id),
  396. ('sale_order_template_id', '=', self.id),
  397. ('date_order', '>=', order_datetime),
  398. ('date_order', '<', order_datetime + relativedelta(months=1)),
  399. ]
  400. existing_order = SaleOrder.search(domain, limit=1)
  401. # Obtener mapeo de proyectos según el modo de operación
  402. project_map = self._get_project_map(order_datetime, sale_order=existing_order if existing_order else None)
  403. if existing_order:
  404. # Nunca tocar pedidos finalizados o cancelados por el usuario.
  405. if existing_order.state in ['done', 'cancel']:
  406. continue
  407. # Si hay facturas validadas o pagadas, no podemos proceder automáticamente.
  408. # Se registra un aviso para intervención manual.
  409. if any(inv.state not in ('draft', 'cancel') for inv in existing_order.invoice_ids):
  410. _logger.warning(
  411. "Se omite la actualización del pedido %s desde la plantilla %s. "
  412. "Tiene facturas validadas que requieren intervención manual.",
  413. existing_order.name, self.name
  414. )
  415. continue
  416. # --- Lógica de actualización de líneas usando el campo template_line_id ---
  417. existing_order.invoice_ids.filtered(lambda inv: inv.state == 'draft').unlink()
  418. commands = []
  419. template_lines = self.sale_order_template_line_ids.filtered(lambda l: not l.display_type)
  420. order_lines_with_link = existing_order.order_line.filtered('template_line_id')
  421. template_lines_map = {t_line.id: t_line for t_line in template_lines}
  422. order_lines_map = {o_line.template_line_id.id: o_line for o_line in order_lines_with_link}
  423. # 1. Actualizar líneas existentes y encontrar nuevas para crear
  424. for t_line_id, t_line in template_lines_map.items():
  425. # Configurar proyecto si es necesario
  426. self._configure_project_for_contract(t_line)
  427. vals = t_line._prepare_order_line_values()
  428. # Si monthly_invoice_project está activo y la línea tiene proyecto, asignar el proyecto mensual
  429. if self.monthly_invoice_project and t_line.project_id and t_line.project_id.id in project_map:
  430. project_month = project_map[t_line.project_id.id]
  431. vals['project_id'] = project_month.id
  432. # Asignar la cuenta analítica del proyecto mensual a la distribución analítica
  433. if project_month.account_id:
  434. vals['analytic_distribution'] = {project_month.account_id.id: 100}
  435. if t_line_id in order_lines_map:
  436. o_line = order_lines_map[t_line_id]
  437. # Comparar campos clave para ver si se necesita una actualización.
  438. # Si el nombre en la plantilla está vacío, no se considera para la actualización,
  439. # permitiendo que el nombre en la línea del pedido (ya sea el default del producto
  440. # o uno modificado manualmente) se preserve.
  441. price_unit_in_template = vals.get('price_unit')
  442. name_in_template = vals.get('name')
  443. qty_in_template = vals.get('product_uom_qty')
  444. # Determinar el precio unitario objetivo.
  445. # Si la plantilla especifica un precio (distinto de cero), se usa ese.
  446. # Si no, se recalcula para obtener el precio por defecto (según lista de precios).
  447. target_price_unit = 0.0
  448. if price_unit_in_template:
  449. target_price_unit = price_unit_in_template
  450. else:
  451. # Recalcular para obtener el precio por defecto.
  452. new_sol = self.env['sale.order.line'].new({
  453. 'order_id': existing_order.id, 'product_id': o_line.product_id.id,
  454. 'product_uom': o_line.product_uom.id, 'product_uom_qty': qty_in_template,
  455. 'order_partner_id': existing_order.partner_id.id,
  456. })
  457. new_sol._compute_price_unit()
  458. target_price_unit = new_sol.price_unit
  459. # Verificar si hay cambios que requieran actualización
  460. needs_update = (
  461. o_line.product_uom_qty != qty_in_template or
  462. (name_in_template and o_line.name != name_in_template) or
  463. o_line.price_unit != target_price_unit or
  464. o_line.project_id != t_line.project_id
  465. )
  466. # Verificar cambios en distribución analítica
  467. analytic_distribution_in_template = vals.get('analytic_distribution', {})
  468. if o_line.analytic_distribution != analytic_distribution_in_template:
  469. needs_update = True
  470. if needs_update:
  471. update_payload = {
  472. 'product_uom_qty': qty_in_template,
  473. 'price_unit': target_price_unit,
  474. 'analytic_distribution': analytic_distribution_in_template,
  475. }
  476. if name_in_template:
  477. update_payload['name'] = name_in_template
  478. # Si monthly_invoice_project está activo y la línea tiene proyecto, asignar el proyecto mensual
  479. if self.monthly_invoice_project and t_line.project_id and t_line.project_id.id in project_map:
  480. project_month = project_map[t_line.project_id.id]
  481. update_payload['project_id'] = project_month.id
  482. # Asignar la cuenta analítica del proyecto mensual a la distribución analítica
  483. if project_month.account_id:
  484. update_payload['analytic_distribution'] = {project_month.account_id.id: 100}
  485. elif o_line.project_id != t_line.project_id:
  486. update_payload['project_id'] = t_line.project_id.id if t_line.project_id else False
  487. commands.append(Command.update(o_line.id, update_payload))
  488. del order_lines_map[t_line_id] # Marcar como procesada
  489. else:
  490. # Línea nueva en la plantilla, crearla en el pedido
  491. commands.append(Command.create(vals))
  492. # 2. Poner en cero las líneas que ya no están en la plantilla
  493. for o_line in order_lines_map.values():
  494. commands.append(Command.update(o_line.id, {'product_uom_qty': 0}))
  495. # 3. Poner en cero las líneas de anticipo existentes para que se recreen
  496. for dp_line in existing_order.order_line.filtered('is_downpayment'):
  497. commands.append(Command.update(dp_line.id, {'product_uom_qty': 0, 'price_unit': 0}))
  498. # 4. Escribir todos los cambios de líneas en una sola operación
  499. if commands:
  500. existing_order.write({'order_line': commands})
  501. # 5. Actualizar campos del pedido y la fecha
  502. update_vals = {'date_order': order_datetime}
  503. if self.payment_term_id and existing_order.payment_term_id != self.payment_term_id:
  504. update_vals['payment_term_id'] = self.payment_term_id.id
  505. if self.opportunity_id and existing_order.opportunity_id != self.opportunity_id:
  506. update_vals['opportunity_id'] = self.opportunity_id.id
  507. # Actualizar el vendedor si hay oportunidad
  508. if self.opportunity_id and self.opportunity_id.user_id:
  509. user_id = self.opportunity_id.user_id.id
  510. if existing_order.user_id.id != user_id:
  511. update_vals['user_id'] = user_id
  512. existing_order.write(update_vals)
  513. # Procesar orden y proyectos
  514. self._process_order_and_projects(existing_order, project_map)
  515. else:
  516. # Crear nuevo pedido
  517. # Usar siempre el contract_partner_id para el cliente del pedido
  518. partner = self.contract_partner_id
  519. pricelist = partner.property_product_pricelist
  520. # Priorizar término de pago de la plantilla, si no, el del partner
  521. payment_term = self.payment_term_id or partner.property_payment_term_id
  522. # Configurar proyectos antes de crear líneas
  523. for line in self.sale_order_template_line_ids:
  524. self._configure_project_for_contract(line)
  525. # Obtener mapeo de proyectos
  526. project_map = self._get_project_map(order_datetime)
  527. order_lines_vals = []
  528. for line in self.sale_order_template_line_ids:
  529. vals = line._prepare_order_line_values()
  530. if self.monthly_invoice_project and line.project_id and line.project_id.id in project_map:
  531. project_month = project_map[line.project_id.id]
  532. vals['project_id'] = project_month.id
  533. # Asignar la cuenta analítica del proyecto mensual a la distribución analítica
  534. if project_month.account_id:
  535. vals['analytic_distribution'] = {project_month.account_id.id: 100}
  536. order_lines_vals.append(vals)
  537. # Determinar el vendedor: usar opportunity_id.user_id si existe
  538. user_id = False
  539. if self.opportunity_id and self.opportunity_id.user_id:
  540. user_id = self.opportunity_id.user_id.id
  541. order_vals = {
  542. 'partner_id': partner.id,
  543. 'sale_order_template_id': self.id,
  544. 'date_order': order_datetime,
  545. 'pricelist_id': pricelist.id if pricelist else False,
  546. 'payment_term_id': payment_term.id if payment_term else False,
  547. 'company_id': (self.company_id or self.env.company).id,
  548. 'opportunity_id': self.opportunity_id.id if self.opportunity_id else False,
  549. 'user_id': user_id,
  550. 'order_line': [Command.create(vals) for vals in order_lines_vals]
  551. }
  552. new_order = SaleOrder.create(order_vals)
  553. # Procesar orden y proyectos
  554. self._process_order_and_projects(new_order, project_map)
  555. def create_or_update_downpayment_invoice(self, order):
  556. """
  557. Borra facturas y líneas de adelanto draft, crea un adelanto del 100% con la fecha igual a la orden y confirma la orden y la factura.
  558. """
  559. # 1. Eliminar facturas draft relacionadas a la orden
  560. draft_invoices = order.invoice_ids.filtered(lambda inv: inv.state == 'draft')
  561. draft_invoices.unlink()
  562. # 2. Las líneas de adelanto antiguas ya se han puesto a cero en la lógica de actualización.
  563. # No se deben borrar (`unlink`) de un pedido confirmado.
  564. # 3. Confirmar la orden si está en borrador
  565. if order.state in ['draft', 'sent']:
  566. original_date = order.date_order
  567. order.action_confirm() # Esto cambiará la fecha de la orden al momento actual
  568. order.write({'date_order': original_date}) # Restaurar la fecha original
  569. # 4. Crear adelanto del 100%
  570. wizard = self.env['sale.advance.payment.inv'].create({
  571. 'advance_payment_method': 'percentage',
  572. 'amount': 100,
  573. 'sale_order_ids': [(6, 0, [order.id])],
  574. })
  575. invoices = wizard._create_invoices(order)
  576. # 5. Forzar la fecha de la factura igual a la orden
  577. if invoices:
  578. invoice_date = order.date_order.date()
  579. invoices.write({'invoice_date': invoice_date, 'date': invoice_date})
  580. # 6. Confirmar la factura
  581. # if invoices:
  582. # invoices.action_post()