sale_order_template.py 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256
  1. # -*- coding: utf-8 -*-
  2. from odoo import models, fields, api, _
  3. from odoo.exceptions import UserError
  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. _logger = logging.getLogger(__name__)
  9. class SaleOrderTemplate(models.Model):
  10. _inherit = 'sale.order.template'
  11. use_contract_partner = fields.Boolean(
  12. string='Usar Partner de Contrato',
  13. help="Marque esta casilla para asociar un Partner específico a esta plantilla y sus líneas."
  14. )
  15. contract_partner_id = fields.Many2one(
  16. 'res.partner',
  17. string='Partner del Contrato',
  18. help="Partner opcional asociado a esta plantilla de presupuesto."
  19. )
  20. payment_term_id = fields.Many2one(
  21. 'account.payment.term',
  22. string='Términos de Pago',
  23. company_dependent=True,
  24. help="Términos de pago que se aplicarán a los pedidos generados desde esta plantilla."
  25. )
  26. date_start = fields.Date(
  27. string='Fecha de Inicio del Contrato',
  28. help="Fecha de inicio para el rango del contrato de esta plantilla."
  29. )
  30. date_end = fields.Date(
  31. string='Fecha de Fin del Contrato',
  32. help="Fecha de finalización para el rango del contrato de esta plantilla."
  33. )
  34. def action_generate_contract_orders(self):
  35. """
  36. Acción disparada por el botón para generar o actualizar pedidos de contrato.
  37. """
  38. for template in self:
  39. if not template.use_contract_partner or not template.contract_partner_id or \
  40. not template.date_start or not template.date_end:
  41. 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."))
  42. try:
  43. template._generate_or_update_contract_orders()
  44. except Exception as e:
  45. raise UserError(_("Ocurrió un error al generar los pedidos: %s") % str(e))
  46. return True # Opcional: puedes devolver una acción de ventana si quieres.
  47. def _get_contract_order_dates(self):
  48. """
  49. Calcula las fechas (primer día de cada mes) para las cuales se deben generar pedidos.
  50. """
  51. self.ensure_one()
  52. if not self.date_start or not self.date_end or self.date_start > self.date_end:
  53. return []
  54. order_dates = []
  55. current_date = fields.Date.today()
  56. # Determinar la fecha de inicio real para la generación
  57. # Debe ser el primer día del mes de self.date_start o el primer día del mes actual,
  58. # lo que sea posterior.
  59. actual_start_date = self.date_start.replace(day=1)
  60. # Asegurarse de que la fecha de inicio real no sea posterior a date_end
  61. if actual_start_date > self.date_end:
  62. return []
  63. ptr_date = actual_start_date
  64. while ptr_date <= self.date_end:
  65. order_dates.append(ptr_date)
  66. ptr_date += relativedelta(months=1)
  67. # Asegurarse de que ptr_date siga siendo el primer día del mes
  68. if ptr_date.day != 1: # Esto puede pasar si el mes original tenía más días
  69. ptr_date = ptr_date.replace(day=1)
  70. return order_dates
  71. def _generate_or_update_contract_orders(self):
  72. self.ensure_one()
  73. SaleOrder = self.env['sale.order']
  74. if not self.use_contract_partner or not self.contract_partner_id or not self.date_start or not self.date_end:
  75. # No hacer nada si no están las condiciones
  76. return
  77. order_dates = self._get_contract_order_dates()
  78. for order_date in order_dates:
  79. # Convertir a datetime a las 12:00 para evitar desfases de zona horaria
  80. order_datetime = datetime.combine(order_date, time(12, 0, 0))
  81. # Buscar un pedido existente para este partner, plantilla y mes/año.
  82. # Usar sale_order_template_id es clave para identificar unívocamente el pedido
  83. # y evitar duplicados, sin importar el estado del pedido.
  84. domain = [
  85. ('partner_id', '=', self.contract_partner_id.id),
  86. ('sale_order_template_id', '=', self.id),
  87. ('date_order', '>=', order_datetime),
  88. ('date_order', '<', order_datetime + relativedelta(months=1)),
  89. ]
  90. existing_order = SaleOrder.search(domain, limit=1)
  91. if existing_order:
  92. # Nunca tocar pedidos finalizados o cancelados por el usuario.
  93. if existing_order.state in ['done', 'cancel']:
  94. continue
  95. # Si hay facturas validadas o pagadas, no podemos proceder automáticamente.
  96. # Se registra un aviso para intervención manual.
  97. if any(inv.state not in ('draft', 'cancel') for inv in existing_order.invoice_ids):
  98. _logger.warning(
  99. "Se omite la actualización del pedido %s desde la plantilla %s. "
  100. "Tiene facturas validadas que requieren intervención manual.",
  101. existing_order.name, self.name
  102. )
  103. continue
  104. # --- Lógica de actualización de líneas usando el campo template_line_id ---
  105. existing_order.invoice_ids.filtered(lambda inv: inv.state == 'draft').unlink()
  106. commands = []
  107. template_lines = self.sale_order_template_line_ids.filtered(lambda l: not l.display_type)
  108. order_lines_with_link = existing_order.order_line.filtered('template_line_id')
  109. template_lines_map = {t_line.id: t_line for t_line in template_lines}
  110. order_lines_map = {o_line.template_line_id.id: o_line for o_line in order_lines_with_link}
  111. # 1. Actualizar líneas existentes y encontrar nuevas para crear
  112. for t_line_id, t_line in template_lines_map.items():
  113. vals = t_line._prepare_order_line_values()
  114. if t_line_id in order_lines_map:
  115. o_line = order_lines_map[t_line_id]
  116. # Comparar campos clave para ver si se necesita una actualización.
  117. # Si el nombre en la plantilla está vacío, no se considera para la actualización,
  118. # permitiendo que el nombre en la línea del pedido (ya sea el default del producto
  119. # o uno modificado manualmente) se preserve.
  120. price_unit_in_template = vals.get('price_unit')
  121. name_in_template = vals.get('name')
  122. qty_in_template = vals.get('product_uom_qty')
  123. # Determinar el precio unitario objetivo.
  124. # Si la plantilla especifica un precio (distinto de cero), se usa ese.
  125. # Si no, se recalcula para obtener el precio por defecto (según lista de precios).
  126. target_price_unit = 0.0
  127. if price_unit_in_template:
  128. target_price_unit = price_unit_in_template
  129. else:
  130. # Recalcular para obtener el precio por defecto.
  131. new_sol = self.env['sale.order.line'].new({
  132. 'order_id': existing_order.id, 'product_id': o_line.product_id.id,
  133. 'product_uom': o_line.product_uom.id, 'product_uom_qty': qty_in_template,
  134. 'order_partner_id': existing_order.partner_id.id,
  135. })
  136. new_sol._compute_price_unit()
  137. target_price_unit = new_sol.price_unit
  138. # También actualizar si cambia el project_id
  139. if (o_line.product_uom_qty != qty_in_template or
  140. (name_in_template and o_line.name != name_in_template) or
  141. o_line.price_unit != target_price_unit or
  142. o_line.project_id != t_line.project_id):
  143. update_payload = {
  144. 'product_uom_qty': qty_in_template,
  145. 'price_unit': target_price_unit,
  146. }
  147. if name_in_template:
  148. update_payload['name'] = name_in_template
  149. if o_line.project_id != t_line.project_id:
  150. update_payload['project_id'] = t_line.project_id.id if t_line.project_id else False
  151. commands.append(Command.update(o_line.id, update_payload))
  152. del order_lines_map[t_line_id] # Marcar como procesada
  153. else:
  154. # Línea nueva en la plantilla, crearla en el pedido
  155. commands.append(Command.create(vals))
  156. # 2. Poner en cero las líneas que ya no están en la plantilla
  157. for o_line in order_lines_map.values():
  158. commands.append(Command.update(o_line.id, {'product_uom_qty': 0}))
  159. # 3. Poner en cero las líneas de anticipo existentes para que se recreen
  160. for dp_line in existing_order.order_line.filtered('is_downpayment'):
  161. commands.append(Command.update(dp_line.id, {'product_uom_qty': 0, 'price_unit': 0}))
  162. # 4. Escribir todos los cambios de líneas en una sola operación
  163. if commands:
  164. existing_order.write({'order_line': commands})
  165. # Forzar recálculo del campo 'code' en las líneas de pedido actualizadas
  166. existing_order.order_line._fields['code'].recompute(existing_order.order_line)
  167. # 5. Actualizar campos del pedido y la fecha
  168. update_vals = {'date_order': order_datetime}
  169. if self.payment_term_id and existing_order.payment_term_id != self.payment_term_id:
  170. update_vals['payment_term_id'] = self.payment_term_id.id
  171. existing_order.write(update_vals)
  172. self.create_or_update_downpayment_invoice(existing_order)
  173. else:
  174. # Crear nuevo pedido
  175. pricelist = self.contract_partner_id.property_product_pricelist
  176. # Priorizar término de pago de la plantilla, si no, el del partner
  177. payment_term = self.payment_term_id or self.contract_partner_id.property_payment_term_id
  178. order_lines_vals = [line._prepare_order_line_values() for line in self.sale_order_template_line_ids]
  179. order_vals = {
  180. 'partner_id': self.contract_partner_id.id,
  181. 'sale_order_template_id': self.id,
  182. 'date_order': order_datetime,
  183. 'pricelist_id': pricelist.id if pricelist else False,
  184. 'payment_term_id': payment_term.id if payment_term else False,
  185. 'company_id': (self.company_id or self.env.company).id,
  186. 'order_line': [Command.create(vals) for vals in order_lines_vals]
  187. }
  188. new_order = SaleOrder.create(order_vals)
  189. self.create_or_update_downpayment_invoice(new_order)
  190. def create_or_update_downpayment_invoice(self, order):
  191. """
  192. 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.
  193. """
  194. # 1. Eliminar facturas draft relacionadas a la orden
  195. draft_invoices = order.invoice_ids.filtered(lambda inv: inv.state == 'draft')
  196. draft_invoices.unlink()
  197. # 2. Las líneas de adelanto antiguas ya se han puesto a cero en la lógica de actualización.
  198. # No se deben borrar (`unlink`) de un pedido confirmado.
  199. # 3. Confirmar la orden si está en borrador
  200. if order.state in ['draft', 'sent']:
  201. original_date = order.date_order
  202. order.action_confirm() # Esto cambiará la fecha de la orden al momento actual
  203. order.write({'date_order': original_date}) # Restaurar la fecha original
  204. # 4. Crear adelanto del 100%
  205. wizard = self.env['sale.advance.payment.inv'].create({
  206. 'advance_payment_method': 'percentage',
  207. 'amount': 100,
  208. 'sale_order_ids': [(6, 0, [order.id])],
  209. })
  210. invoices = wizard._create_invoices(order)
  211. # 5. Forzar la fecha de la factura igual a la orden
  212. if invoices:
  213. invoice_date = order.date_order.date()
  214. invoices.write({'invoice_date': invoice_date, 'date': invoice_date})
  215. # 6. Confirmar la factura
  216. # if invoices:
  217. # invoices.action_post()