171 lines
8.1 KiB
Python
171 lines
8.1 KiB
Python
# -*- coding: utf-8 -*-
|
|
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
|
from datetime import datetime, time
|
|
|
|
from dateutil.relativedelta import relativedelta
|
|
from pytz import timezone, UTC
|
|
|
|
from odoo import models, api, fields
|
|
from odoo.tools import format_date, clean_context
|
|
|
|
|
|
from odoo.addons.sale_subscription.models.sale_order import SUBSCRIPTION_PROGRESS_STATE
|
|
|
|
|
|
class SaleOrderLine(models.Model):
|
|
_inherit = "sale.order.line"
|
|
|
|
# =============================
|
|
# Compute
|
|
# =============================
|
|
|
|
@api.depends('order_id.next_invoice_date', 'order_id.start_date')
|
|
def _compute_qty_delivered(self):
|
|
return super()._compute_qty_delivered()
|
|
|
|
# =============================
|
|
# Utils
|
|
# =============================
|
|
|
|
def _get_outgoing_incoming_moves(self, strict=True):
|
|
""" Get the moves which are related to the given period.
|
|
"""
|
|
sub_stock = self._get_stock_subscription_lines()
|
|
outgoing_moves, incoming_moves = super(SaleOrderLine, self - sub_stock)._get_outgoing_incoming_moves(strict)
|
|
for line in sub_stock:
|
|
period_start = line.order_id.last_invoice_date or line.order_id.start_date
|
|
if period_start and period_start == line.order_id.next_invoice_date:
|
|
period_end = period_start + line.order_id.plan_id.billing_period
|
|
else:
|
|
period_end = line.order_id.next_invoice_date
|
|
sub_outgoing_moves, sub_incoming_moves = super(SaleOrderLine, line)._get_outgoing_incoming_moves(strict)
|
|
|
|
def date_filter(m):
|
|
return (
|
|
m.date
|
|
and (not period_start or period_start <= m.date.date())
|
|
and (not period_end or m.date.date() <= period_end)
|
|
)
|
|
sub_outgoing_moves, sub_incoming_moves = sub_outgoing_moves.filtered(date_filter), sub_incoming_moves.filtered(date_filter)
|
|
outgoing_moves += sub_outgoing_moves
|
|
incoming_moves += sub_incoming_moves
|
|
|
|
return outgoing_moves, incoming_moves
|
|
|
|
def _get_stock_subscription_lines(self):
|
|
""" Return the sale.order.line of self which relate to a subscription of storable products
|
|
"""
|
|
return self.filtered(lambda line: line.recurring_invoice and not line._subscription_is_one_time_sale() and line.product_id.type == 'consu')
|
|
|
|
def _get_invoice_line_parameters(self):
|
|
self.ensure_one()
|
|
if self._is_postpaid_line() and self.order_id.subscription_state == '5_renewed':
|
|
aml = self.invoice_lines.filtered(
|
|
lambda l: l.move_id.state == 'posted' and
|
|
l.deferred_start_date and l.deferred_end_date and
|
|
l.deferred_end_date == self.last_invoiced_date)
|
|
if aml:
|
|
return aml[:1].deferred_start_date, aml[:1].deferred_end_date, 1, None
|
|
return super()._get_invoice_line_parameters()
|
|
|
|
# =============================
|
|
# Delivery logic
|
|
# =============================
|
|
|
|
def _reset_subscription_qty_to_invoice(self):
|
|
""" Reset the quantity to deliver for this period
|
|
"""
|
|
super(SaleOrderLine, self)._reset_subscription_qty_to_invoice()
|
|
for line in self._get_stock_subscription_lines():
|
|
line.write({'qty_to_deliver': line.product_uom_qty})
|
|
|
|
def _reset_subscription_quantity_post_invoice(self):
|
|
""" After we invoice, we close the stock.move of product invoiced based on 'delivery'
|
|
for the period we just invoiced and launch stock rule for the next period.
|
|
"""
|
|
stock_subscription_line = self._get_stock_subscription_lines()
|
|
stock_subscription_line.with_context(clean_context(self.env.context))._action_launch_stock_rule()
|
|
return super(SaleOrderLine, self - stock_subscription_line)._reset_subscription_quantity_post_invoice()
|
|
|
|
def _get_lines_to_launch_stock_rule(self):
|
|
stock_line_ids = []
|
|
for line in self:
|
|
if not line.recurring_invoice or line.order_id.subscription_state == '7_upsell':
|
|
# regular lines
|
|
stock_line_ids.append(line.id)
|
|
elif line.qty_invoiced and line.last_invoiced_date:
|
|
# invoiced line (move posted)
|
|
stock_line_ids.append(line.id)
|
|
elif line.order_id.subscription_state in SUBSCRIPTION_PROGRESS_STATE and not line.last_invoiced_date:
|
|
# prepaid and postpaid lines not already invoiced
|
|
stock_line_ids.append(line.id)
|
|
elif line._subscription_is_one_time_sale():
|
|
# one-time sale lines
|
|
stock_line_ids.append(line.id)
|
|
elif line._is_postpaid_line() and line.last_invoiced_date:
|
|
# postpaid already invoiced
|
|
stock_line_ids.append(line.id)
|
|
return self.env['sale.order.line'].browse(stock_line_ids)
|
|
|
|
def _action_launch_stock_rule(self, **kwargs):
|
|
""" Only launch stock rule if we know they won't be empty
|
|
"""
|
|
lines = self._get_lines_to_launch_stock_rule()
|
|
return super(SaleOrderLine, lines)._action_launch_stock_rule(**kwargs)
|
|
|
|
@api.model
|
|
def _get_incoming_outgoing_moves_filter(self):
|
|
"""Check subscription moves in/out during invoice period."""
|
|
base_filter = super()._get_incoming_outgoing_moves_filter()
|
|
if self.recurring_invoice:
|
|
so = self.order_id
|
|
start = so.last_invoice_date or so.start_date
|
|
end = so.next_invoice_date
|
|
|
|
def date_filter(m):
|
|
return m.date and start and end and start <= m.date.date() < end
|
|
return {
|
|
'incoming_moves': lambda m: base_filter['incoming_moves'](m) and date_filter(m),
|
|
'outgoing_moves': lambda m: base_filter['outgoing_moves'](m) and date_filter(m)
|
|
}
|
|
return base_filter
|
|
|
|
def _get_qty_procurement(self, previous_product_uom_qty=False):
|
|
""" Compute the quantity that was already delivered for the current period.
|
|
"""
|
|
self.ensure_one()
|
|
# If we update the line, we don't want it to affect the current period for subscriptions
|
|
if self.recurring_invoice and previous_product_uom_qty and previous_product_uom_qty.get(self.id, 0):
|
|
return self.product_uom_qty
|
|
return super()._get_qty_procurement(previous_product_uom_qty=previous_product_uom_qty)
|
|
|
|
def _prepare_procurement_values(self):
|
|
""" Update move.line values
|
|
We use product_description_variants to display the invoicing date
|
|
Ensure one is present in inherited function
|
|
"""
|
|
values = super()._prepare_procurement_values()
|
|
if not self.recurring_invoice or self.order_id.subscription_state == '7_upsell' or self._subscription_is_one_time_sale():
|
|
return values
|
|
# Remove 1 day as normal people thinks in terms of inclusive ranges.
|
|
if not self.order_id.start_date or self.order_id.next_invoice_date == self.order_id.start_date and not self.order_id.last_invoice_date:
|
|
move_date = self.order_id.next_invoice_date + self.order_id.plan_id.billing_period - relativedelta(days=1)
|
|
else:
|
|
move_date = self.order_id.next_invoice_date - relativedelta(days=1)
|
|
|
|
current_period_start = self.order_id.last_invoice_date or self.order_id.start_date or fields.Date.today()
|
|
lang_code = self.order_id.partner_id.lang
|
|
format_start = format_date(self.env, current_period_start, lang_code=lang_code)
|
|
format_end = format_date(self.env, move_date, lang_code=lang_code)
|
|
if self.order_id.last_invoice_date:
|
|
company_tz = timezone(self.order_id.company_id.partner_id.tz or 'UTC')
|
|
date_planned = company_tz.localize(datetime.combine(current_period_start, time.min)).astimezone(UTC).replace(tzinfo=None)
|
|
else:
|
|
date_planned = self.order_id.date_order or fields.Datetime.now()
|
|
values.update({
|
|
'date_planned': date_planned,
|
|
'date_deadline': move_date,
|
|
'product_description_variants': f'{values.get("product_description_variants", "")}\n{format_start} to {format_end}',
|
|
})
|
|
return values
|