| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256 |
- # -*- coding: utf-8 -*-
- from odoo import models, fields, api, _
- from odoo.exceptions import UserError
- from dateutil.relativedelta import relativedelta
- from datetime import datetime, time
- from odoo import Command # Import Command para operaciones de One2many
- import logging
- _logger = logging.getLogger(__name__)
- 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."
- )
- contract_partner_id = fields.Many2one(
- 'res.partner',
- string='Partner del Contrato',
- help="Partner opcional asociado a esta plantilla de presupuesto."
- )
- payment_term_id = fields.Many2one(
- 'account.payment.term',
- string='Términos de Pago',
- company_dependent=True,
- help="Términos de pago que se aplicarán a los pedidos generados desde esta plantilla."
- )
- date_start = fields.Date(
- string='Fecha de Inicio del Contrato',
- help="Fecha de inicio para el rango del contrato de esta plantilla."
- )
- date_end = fields.Date(
- string='Fecha de Fin del Contrato',
- help="Fecha de finalización para el rango del contrato de esta plantilla."
- )
- def action_generate_contract_orders(self):
- """
- Acción disparada por el botón para generar o actualizar pedidos de contrato.
- """
- for template in self:
- if not template.use_contract_partner or not template.contract_partner_id or \
- not template.date_start or not template.date_end:
- 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."))
- try:
- template._generate_or_update_contract_orders()
- except Exception as 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.
- def _get_contract_order_dates(self):
- """
- Calcula las fechas (primer día de cada mes) para las cuales se deben generar pedidos.
- """
- self.ensure_one()
- if not self.date_start or not self.date_end or self.date_start > self.date_end:
- return []
- order_dates = []
- current_date = fields.Date.today()
-
- # Determinar la fecha de inicio real para la generación
- # Debe ser el primer día del mes de self.date_start o el primer día del mes actual,
- # lo que sea posterior.
- actual_start_date = self.date_start.replace(day=1)
- # Asegurarse de que la fecha de inicio real no sea posterior a date_end
- if actual_start_date > self.date_end:
- return []
- ptr_date = actual_start_date
- while ptr_date <= self.date_end:
- order_dates.append(ptr_date)
- ptr_date += relativedelta(months=1)
- # Asegurarse de que ptr_date siga siendo el primer día del mes
- if ptr_date.day != 1: # Esto puede pasar si el mes original tenía más días
- ptr_date = ptr_date.replace(day=1)
- return order_dates
- def _generate_or_update_contract_orders(self):
- self.ensure_one()
- SaleOrder = self.env['sale.order']
- if not self.use_contract_partner or not self.contract_partner_id or not self.date_start or not self.date_end:
- # No hacer nada si no están las condiciones
- return
- order_dates = self._get_contract_order_dates()
- for order_date in order_dates:
- # Convertir a datetime a las 12:00 para evitar desfases de zona horaria
- order_datetime = datetime.combine(order_date, time(12, 0, 0))
- # Buscar un pedido existente para este partner, plantilla y mes/año.
- # Usar sale_order_template_id es clave para identificar unívocamente el pedido
- # y evitar duplicados, sin importar el estado del pedido.
- domain = [
- ('partner_id', '=', self.contract_partner_id.id),
- ('sale_order_template_id', '=', self.id),
- ('date_order', '>=', order_datetime),
- ('date_order', '<', order_datetime + relativedelta(months=1)),
- ]
- existing_order = SaleOrder.search(domain, limit=1)
- if existing_order:
- # Nunca tocar pedidos finalizados o cancelados por el usuario.
- if existing_order.state in ['done', 'cancel']:
- continue
- # Si hay facturas validadas o pagadas, no podemos proceder automáticamente.
- # Se registra un aviso para intervención manual.
- if any(inv.state not in ('draft', 'cancel') for inv in existing_order.invoice_ids):
- _logger.warning(
- "Se omite la actualización del pedido %s desde la plantilla %s. "
- "Tiene facturas validadas que requieren intervención manual.",
- existing_order.name, self.name
- )
- continue
- # --- Lógica de actualización de líneas usando el campo template_line_id ---
- existing_order.invoice_ids.filtered(lambda inv: inv.state == 'draft').unlink()
- commands = []
- template_lines = self.sale_order_template_line_ids.filtered(lambda l: not l.display_type)
- order_lines_with_link = existing_order.order_line.filtered('template_line_id')
- template_lines_map = {t_line.id: t_line for t_line in template_lines}
- order_lines_map = {o_line.template_line_id.id: o_line for o_line in order_lines_with_link}
- # 1. Actualizar líneas existentes y encontrar nuevas para crear
- for t_line_id, t_line in template_lines_map.items():
- vals = t_line._prepare_order_line_values()
- 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.
- # Si el nombre en la plantilla está vacío, no se considera para la actualización,
- # permitiendo que el nombre en la línea del pedido (ya sea el default del producto
- # o uno modificado manualmente) se preserve.
- price_unit_in_template = vals.get('price_unit')
- name_in_template = vals.get('name')
- qty_in_template = vals.get('product_uom_qty')
- # Determinar el precio unitario objetivo.
- # Si la plantilla especifica un precio (distinto de cero), se usa ese.
- # Si no, se recalcula para obtener el precio por defecto (según lista de precios).
- target_price_unit = 0.0
- if price_unit_in_template:
- target_price_unit = price_unit_in_template
- else:
- # Recalcular para obtener el precio por defecto.
- new_sol = self.env['sale.order.line'].new({
- 'order_id': existing_order.id, 'product_id': o_line.product_id.id,
- 'product_uom': o_line.product_uom.id, 'product_uom_qty': qty_in_template,
- 'order_partner_id': existing_order.partner_id.id,
- })
- 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):
- update_payload = {
- 'product_uom_qty': qty_in_template,
- 'price_unit': target_price_unit,
- }
- if name_in_template:
- update_payload['name'] = name_in_template
- if 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))
- del order_lines_map[t_line_id] # Marcar como procesada
- else:
- # Línea nueva en la plantilla, crearla en el pedido
- commands.append(Command.create(vals))
- # 2. Poner en cero las líneas que ya no están en la plantilla
- for o_line in order_lines_map.values():
- commands.append(Command.update(o_line.id, {'product_uom_qty': 0}))
- # 3. Poner en cero las líneas de anticipo existentes para que se recreen
- for dp_line in existing_order.order_line.filtered('is_downpayment'):
- commands.append(Command.update(dp_line.id, {'product_uom_qty': 0, 'price_unit': 0}))
- # 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}
- if self.payment_term_id and existing_order.payment_term_id != self.payment_term_id:
- update_vals['payment_term_id'] = self.payment_term_id.id
- existing_order.write(update_vals)
- self.create_or_update_downpayment_invoice(existing_order)
- 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]
- order_vals = {
- 'partner_id': self.contract_partner_id.id,
- 'sale_order_template_id': self.id,
- 'date_order': order_datetime,
- 'pricelist_id': pricelist.id if pricelist else False,
- 'payment_term_id': payment_term.id if payment_term else False,
- 'company_id': (self.company_id or self.env.company).id,
- '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)
- def create_or_update_downpayment_invoice(self, order):
- """
- 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.
- """
- # 1. Eliminar facturas draft relacionadas a la orden
- draft_invoices = order.invoice_ids.filtered(lambda inv: inv.state == 'draft')
- draft_invoices.unlink()
- # 2. Las líneas de adelanto antiguas ya se han puesto a cero en la lógica de actualización.
- # No se deben borrar (`unlink`) de un pedido confirmado.
- # 3. Confirmar la orden si está en borrador
- if order.state in ['draft', 'sent']:
- original_date = order.date_order
- order.action_confirm() # Esto cambiará la fecha de la orden al momento actual
- order.write({'date_order': original_date}) # Restaurar la fecha original
- # 4. Crear adelanto del 100%
- wizard = self.env['sale.advance.payment.inv'].create({
- 'advance_payment_method': 'percentage',
- 'amount': 100,
- 'sale_order_ids': [(6, 0, [order.id])],
- })
- invoices = wizard._create_invoices(order)
- # 5. Forzar la fecha de la factura igual a la orden
- if invoices:
- invoice_date = order.date_order.date()
- invoices.write({'invoice_date': invoice_date, 'date': invoice_date})
- # 6. Confirmar la factura
- # if invoices:
- # invoices.action_post()
|