Files
tryton/modules/sale_subscription/subscription.py
T

1026 lines
37 KiB
Python
Raw Normal View History

2017-04-02 00:40:05 +02:00
# This file is part of Tryton. The COPYRIGHT file at the top level of
# this repository contains the full copyright notices and license terms.
import datetime
from itertools import groupby
2021-12-11 17:58:44 +01:00
from sql import Literal, Null, operators
from sql.conditionals import Case, Coalesce
from sql.functions import CharLength
2017-04-02 00:40:05 +02:00
2018-12-29 14:20:29 +01:00
from trytond.i18n import gettext
2021-12-11 17:58:44 +01:00
from trytond.model import (
Index, ModelSQL, ModelView, Workflow, fields, sequence_ordered)
2018-12-29 14:20:29 +01:00
from trytond.model.exceptions import AccessError
2020-04-10 18:53:44 +02:00
from trytond.modules.company.model import (
2021-12-11 17:58:44 +01:00
employee_field, reset_employee, set_employee)
2021-08-21 09:09:56 +02:00
from trytond.modules.currency.fields import Monetary
2023-08-17 17:57:32 +02:00
from trytond.modules.product import price_digits
2021-12-11 17:58:44 +01:00
from trytond.pool import Pool
from trytond.pyson import Bool, Eval, If
from trytond.tools import sortable_values
from trytond.transaction import Transaction
from trytond.wizard import (
Button, StateAction, StateTransition, StateView, Wizard)
from .exceptions import InvalidRecurrence, InvoiceError
2017-04-02 00:40:05 +02:00
class Subscription(Workflow, ModelSQL, ModelView):
"Subscription"
__name__ = 'sale.subscription'
_rec_name = 'number'
company = fields.Many2One(
'company.company', "Company", required=True,
2017-04-02 00:40:05 +02:00
states={
'readonly': (Eval('state') != 'draft') | Eval('party', True),
2017-04-02 00:40:05 +02:00
},
help="Make the subscription belong to the company.")
number = fields.Char(
"Number", readonly=True,
2017-04-02 00:40:05 +02:00
help="The main identification of the subscription.")
reference = fields.Char(
"Reference",
2017-04-02 00:40:05 +02:00
help="The identification of an external origin.")
description = fields.Char("Description",
states={
'readonly': Eval('state') != 'draft',
})
2017-04-02 00:40:05 +02:00
party = fields.Many2One(
'party.party', "Party", required=True,
states={
'readonly': ((Eval('state') != 'draft')
| (Eval('lines', [0]) & Eval('party'))),
},
context={
'company': Eval('company', -1),
},
2022-04-10 17:47:38 +02:00
depends={'company'},
2017-04-02 00:40:05 +02:00
help="The party who subscribes.")
2020-03-28 14:25:57 +01:00
contact = fields.Many2One(
'party.contact_mechanism', "Contact",
context={
'company': Eval('company', -1),
},
2020-03-28 14:25:57 +01:00
search_context={
'related_party': Eval('party'),
},
depends={'company', 'party'})
2020-03-28 14:25:57 +01:00
invoice_party = fields.Many2One('party.party', "Invoice Party",
states={
'readonly': ((Eval('state') != 'draft')
| Eval('lines', [0])),
},
context={
'company': Eval('company', -1),
},
search_context={
'related_party': Eval('party'),
},
depends={'company', 'party'})
2017-04-02 00:40:05 +02:00
invoice_address = fields.Many2One(
'party.address', "Invoice Address",
domain=[
2020-03-28 14:25:57 +01:00
('party', '=', If(Bool(Eval('invoice_party',)),
Eval('invoice_party'), Eval('party'))),
2017-04-02 00:40:05 +02:00
],
states={
'readonly': Eval('state') != 'draft',
'required': ~Eval('state').in_(['draft']),
})
2017-04-02 00:40:05 +02:00
payment_term = fields.Many2One(
2023-02-21 12:55:12 +01:00
'account.invoice.payment_term', "Payment Term", ondelete='RESTRICT',
2017-04-02 00:40:05 +02:00
states={
'readonly': Eval('state') != 'draft',
})
2017-04-02 00:40:05 +02:00
currency = fields.Many2One(
'currency.currency', "Currency", required=True,
states={
'readonly': ((Eval('state') != 'draft')
| (Eval('lines', [0]) & Eval('currency', 0))),
})
2017-04-02 00:40:05 +02:00
start_date = fields.Date(
"Start Date", required=True,
states={
'readonly': ((Eval('state') != 'draft')
| Eval('next_invoice_date')),
})
2017-04-02 00:40:05 +02:00
end_date = fields.Date(
"End Date",
domain=['OR',
('end_date', '>=', If(
Bool(Eval('start_date')),
Eval('start_date', datetime.date.min),
datetime.date.min)),
('end_date', '=', None),
],
states={
'readonly': Eval('state') != 'draft',
})
2017-04-02 00:40:05 +02:00
invoice_recurrence = fields.Many2One(
'sale.subscription.recurrence.rule.set', "Invoice Recurrence",
required=True,
states={
'readonly': Eval('state') != 'draft',
})
2017-04-02 00:40:05 +02:00
invoice_start_date = fields.Date("Invoice Start Date",
states={
'readonly': ((Eval('state') != 'draft')
| Eval('next_invoice_date')),
})
next_invoice_date = fields.Date(
"Next Invoice Date", readonly=True,
states={
'invisible': Eval('state') != 'running',
})
2017-04-02 00:40:05 +02:00
lines = fields.One2Many(
'sale.subscription.line', 'subscription', "Lines",
states={
2018-06-24 16:41:43 +02:00
'readonly': ((Eval('state') != 'draft')
| ~Eval('start_date')),
})
2017-04-02 00:40:05 +02:00
2020-04-10 18:53:44 +02:00
quoted_by = employee_field(
"Quoted By", states=['quotation', 'running', 'closed', 'cancelled'])
2020-04-10 18:53:44 +02:00
run_by = employee_field(
"Run By", states=['running', 'closed', 'cancelled'])
state = fields.Selection([
('draft', "Draft"),
('quotation', "Quotation"),
('running', "Running"),
('closed', "Closed"),
('cancelled', "Cancelled"),
2022-02-15 00:08:47 +01:00
], "State", readonly=True, required=True, sort=False,
2017-04-02 00:40:05 +02:00
help="The current state of the subscription.")
@classmethod
def __setup__(cls):
cls.number.search_unaccented = False
cls.reference.search_unaccented = False
2017-04-02 00:40:05 +02:00
super(Subscription, cls).__setup__()
t = cls.__table__()
cls._sql_indexes.update({
Index(t, (t.reference, Index.Similarity())),
Index(
t,
(t.state, Index.Equality()),
where=t.state.in_(['draft', 'quotation', 'running'])),
})
2017-04-02 00:40:05 +02:00
cls._order = [
('start_date', 'DESC'),
('id', 'DESC'),
]
cls._transitions |= set((
('draft', 'cancelled'),
2017-04-02 00:40:05 +02:00
('draft', 'quotation'),
('quotation', 'cancelled'),
2017-04-02 00:40:05 +02:00
('quotation', 'draft'),
('quotation', 'running'),
('running', 'draft'),
('running', 'closed'),
('cancelled', 'draft'),
2017-04-02 00:40:05 +02:00
))
cls._buttons.update({
'cancel': {
'invisible': ~Eval('state').in_(['draft', 'quotation']),
'icon': 'tryton-cancel',
2018-02-19 11:41:34 +01:00
'depends': ['state'],
2017-04-02 00:40:05 +02:00
},
'draft': {
'invisible': Eval('state').in_(['draft', 'closed']),
'icon': If(Eval('state') == 'cancelled',
2018-08-19 20:02:58 +02:00
'tryton-undo', 'tryton-back'),
2018-02-19 11:41:34 +01:00
'depends': ['state'],
2017-04-02 00:40:05 +02:00
},
'quote': {
'invisible': Eval('state') != 'draft',
'readonly': ~Eval('lines', []),
2018-08-19 20:02:58 +02:00
'icon': 'tryton-forward',
2018-02-19 11:41:34 +01:00
'depends': ['state'],
2017-04-02 00:40:05 +02:00
},
'run': {
'invisible': Eval('state') != 'quotation',
2018-08-19 20:02:58 +02:00
'icon': 'tryton-forward',
2018-02-19 11:41:34 +01:00
'depends': ['state'],
2017-04-02 00:40:05 +02:00
},
})
@classmethod
def __register__(cls, module_name):
cursor = Transaction().connection.cursor()
table = cls.__table__()
super().__register__(module_name)
# Migration from 5.6: rename state canceled to cancelled
cursor.execute(*table.update(
[table.state], ['cancelled'],
where=table.state == 'canceled'))
@classmethod
def order_number(cls, tables):
table, _ = tables[None]
return [CharLength(table.number), table.number]
2017-04-02 00:40:05 +02:00
@classmethod
def default_company(cls):
return Transaction().context.get('company')
@classmethod
2022-04-11 21:24:28 +02:00
def default_currency(cls, **pattern):
2017-04-02 00:40:05 +02:00
pool = Pool()
Company = pool.get('company.company')
2022-04-11 21:24:28 +02:00
company = pattern.get('company')
if not company:
company = cls.default_company()
2017-04-02 00:40:05 +02:00
if company:
return Company(company).currency.id
@classmethod
def default_state(cls):
return 'draft'
2022-04-11 21:24:28 +02:00
@fields.depends('company', 'party', 'invoice_party', 'currency', 'lines')
2017-04-02 00:40:05 +02:00
def on_change_party(self):
2020-03-28 14:25:57 +01:00
if not self.invoice_party:
self.invoice_address = None
2022-04-11 21:24:28 +02:00
if not self.lines:
self.currency = self.default_currency(
company=self.company.id if self.company else None)
2017-04-02 00:40:05 +02:00
if self.party:
2020-03-28 14:25:57 +01:00
if not self.invoice_party:
self.invoice_address = self.party.address_get(type='invoice')
2017-04-02 00:40:05 +02:00
self.payment_term = self.party.customer_payment_term
2022-04-11 21:24:28 +02:00
if not self.lines and self.party.customer_currency:
self.currency = self.party.customer_currency
2017-04-02 00:40:05 +02:00
2020-03-28 14:25:57 +01:00
@fields.depends('party', 'invoice_party')
def on_change_invoice_party(self):
if self.invoice_party:
self.invoice_address = self.invoice_party.address_get(
type='invoice')
elif self.party:
self.invoice_address = self.party.address_get(type='invoice')
2017-04-02 00:40:05 +02:00
@classmethod
def set_number(cls, subscriptions):
pool = Pool()
Config = pool.get('sale.configuration')
config = Config(1)
for subscription in subscriptions:
if subscription.number:
continue
2021-04-03 14:27:10 +02:00
subscription.number = config.get_multivalue(
'subscription_sequence',
company=subscription.company.id).get()
2017-04-02 00:40:05 +02:00
cls.save(subscriptions)
def compute_next_invoice_date(self):
start_date = self.invoice_start_date or self.start_date
2020-03-26 09:25:37 +01:00
date = self.next_invoice_date or start_date
2017-04-02 00:40:05 +02:00
rruleset = self.invoice_recurrence.rruleset(start_date)
dt = datetime.datetime.combine(date, datetime.time())
inc = (start_date == date) and not self.next_invoice_date
next_date = rruleset.after(dt, inc=inc)
if next_date:
return next_date.date()
@classmethod
2022-04-11 23:24:21 +02:00
def validate_fields(cls, subscriptions, field_names):
super().validate_fields(subscriptions, field_names)
cls.validate_invoice_recurrence(subscriptions, field_names)
2022-04-11 23:24:21 +02:00
@classmethod
def validate_invoice_recurrence(cls, subscriptions, field_names=None):
if field_names and not (field_names & {
'start_date', 'invoice_start_date', 'invoice_recurrence'}):
return
for subscription in subscriptions:
start_date = (
subscription.invoice_start_date or subscription.start_date)
try:
subscription.invoice_recurrence.rruleset(start_date)[0]
except IndexError:
raise InvalidRecurrence(gettext(
'sale_subscription.msg_invoice_recurrence_invalid',
subscription=subscription.rec_name))
2017-04-02 00:40:05 +02:00
def get_rec_name(self, name):
items = []
if self.number:
items.append(self.number)
if self.reference:
items.append('[%s]' % self.reference)
if not items:
items.append('(%s)' % self.id)
return ' '.join(items)
@classmethod
def search_rec_name(cls, name, clause):
_, operator, value = clause
if operator.startswith('!') or operator.startswith('not '):
bool_op = 'AND'
else:
bool_op = 'OR'
domain = [bool_op,
('number', operator, value),
('reference', operator, value),
]
return domain
2017-04-02 00:40:05 +02:00
@classmethod
def copy(cls, subscriptions, default=None):
if default is None:
default = {}
else:
default = default.copy()
2018-09-01 01:02:04 +02:00
default.setdefault('number', None)
default.setdefault('next_invoice_date', None)
2021-04-05 16:24:13 +02:00
default.setdefault('quoted_by')
default.setdefault('run_by')
2017-04-02 00:40:05 +02:00
return super(Subscription, cls).copy(subscriptions, default=default)
2019-09-20 11:47:15 +02:00
@classmethod
def view_attributes(cls):
2021-04-09 10:52:03 +02:00
return super().view_attributes() + [
('/tree', 'visual',
If(Eval('state') == 'cancelled', 'muted', '')),
2019-09-20 11:47:15 +02:00
]
2017-04-02 00:40:05 +02:00
@classmethod
@ModelView.button
@Workflow.transition('cancelled')
2017-04-02 00:40:05 +02:00
def cancel(cls, subscriptions):
pass
@classmethod
@ModelView.button
@Workflow.transition('draft')
2020-04-10 18:53:44 +02:00
@reset_employee('quoted_by', 'run_by')
2017-04-02 00:40:05 +02:00
def draft(cls, subscriptions):
pass
@classmethod
@ModelView.button
@Workflow.transition('quotation')
2020-04-10 18:53:44 +02:00
@set_employee('quoted_by')
2017-04-02 00:40:05 +02:00
def quote(cls, subscriptions):
cls.set_number(subscriptions)
@classmethod
@ModelView.button
@Workflow.transition('running')
2020-04-10 18:53:44 +02:00
@set_employee('run_by')
2017-04-02 00:40:05 +02:00
def run(cls, subscriptions):
pool = Pool()
Line = pool.get('sale.subscription.line')
lines = []
for subscription in subscriptions:
if not subscription.next_invoice_date:
subscription.next_invoice_date = (
subscription.compute_next_invoice_date())
for line in subscription.lines:
if (line.next_consumption_date is None
and not line.consumed_until):
2017-04-02 00:40:05 +02:00
line.next_consumption_date = (
line.compute_next_consumption_date())
lines.extend(subscription.lines)
Line.save(lines)
cls.save(subscriptions)
@classmethod
def process(cls, subscriptions):
to_close = []
for subscription in subscriptions:
if all(l.next_consumption_date is None
for l in subscription.lines):
to_close.append(subscription)
cls.close(to_close)
@classmethod
@Workflow.transition('closed')
def close(cls, subscriptions):
for subscription in subscriptions:
if not subscription.end_date and subscription.lines:
subscription.end_date = max(
l.end_date for l in subscription.lines)
cls.save(subscriptions)
2017-04-02 00:40:05 +02:00
@classmethod
def generate_invoice(cls, date=None):
pool = Pool()
Date = pool.get('ir.date')
Consumption = pool.get('sale.subscription.line.consumption')
Invoice = pool.get('account.invoice')
InvoiceLine = pool.get('account.invoice.line')
if date is None:
date = Date.today()
company_id = Transaction().context.get('company', -1)
2017-04-02 00:40:05 +02:00
consumptions = Consumption.search([
('invoice_line', '=', None),
('line.subscription.next_invoice_date', '<=', date),
('line.subscription.state', 'in', ['running', 'closed']),
('line.subscription.company', '=', company_id),
2017-04-02 00:40:05 +02:00
],
order=[
('line.subscription.id', 'DESC'),
])
def keyfunc(consumption):
return consumption.line.subscription
invoices = {}
lines = {}
for subscription, consumptions in groupby(consumptions, key=keyfunc):
invoices[subscription] = invoice = subscription._get_invoice()
lines[subscription] = Consumption.get_invoice_lines(
consumptions, invoice)
2018-06-26 19:11:55 +02:00
all_invoices = list(invoices.values())
2017-04-02 00:40:05 +02:00
Invoice.save(all_invoices)
all_invoice_lines = []
2018-06-26 19:11:55 +02:00
for subscription, invoice in invoices.items():
2017-04-02 00:40:05 +02:00
invoice_lines, _ = lines[subscription]
all_invoice_lines.extend(invoice_lines)
InvoiceLine.save(all_invoice_lines)
all_consumptions = []
2018-06-26 19:11:55 +02:00
for values in lines.values():
2017-04-02 00:40:05 +02:00
for invoice_line, consumptions in zip(*values):
for consumption in consumptions:
assert not consumption.invoice_line
consumption.invoice_line = invoice_line
all_consumptions.append(consumption)
2017-09-09 16:30:18 +02:00
Consumption.save(all_consumptions)
2017-04-02 00:40:05 +02:00
Invoice.update_taxes(all_invoices)
subscriptions = cls.search([
('next_invoice_date', '<=', date),
('company', '=', company_id),
2017-04-02 00:40:05 +02:00
])
for subscription in subscriptions:
if subscription.state == 'running':
while subscription.next_invoice_date <= date:
subscription.next_invoice_date = (
subscription.compute_next_invoice_date())
else:
subscription.next_invoice_date = None
cls.save(subscriptions)
def _get_invoice(self):
pool = Pool()
Invoice = pool.get('account.invoice')
2020-03-28 14:25:57 +01:00
party = self.invoice_party or self.party
2017-04-02 00:40:05 +02:00
invoice = Invoice(
company=self.company,
type='out',
2020-03-28 14:25:57 +01:00
party=party,
2017-04-02 00:40:05 +02:00
invoice_address=self.invoice_address,
currency=self.currency,
2020-03-28 14:25:57 +01:00
account=party.account_receivable_used,
2017-04-02 00:40:05 +02:00
)
2021-10-05 13:33:42 +02:00
invoice.invoice_date = self.next_invoice_date
invoice.set_journal()
2017-04-02 00:40:05 +02:00
invoice.payment_term = self.payment_term
return invoice
class Line(sequence_ordered(), ModelSQL, ModelView):
"Subscription Line"
__name__ = 'sale.subscription.line'
subscription = fields.Many2One(
'sale.subscription', "Subscription", required=True, ondelete='CASCADE',
2017-04-02 00:40:05 +02:00
states={
'readonly': ((Eval('subscription_state') != 'draft')
& Bool(Eval('subscription'))),
},
help="Add the line below the subscription.")
subscription_state = fields.Function(
fields.Selection('get_subscription_states', "Subscription State"),
2017-04-02 00:40:05 +02:00
'on_change_with_subscription_state')
subscription_start_date = fields.Function(
fields.Date("Subscription Start Date"),
'on_change_with_subscription_start_date')
subscription_end_date = fields.Function(
fields.Date("Subscription End Date"),
'on_change_with_subscription_end_date')
2020-04-25 09:19:02 +02:00
company = fields.Function(
fields.Many2One('company.company', "Company"),
'on_change_with_company')
2017-04-02 00:40:05 +02:00
service = fields.Many2One(
'sale.subscription.service', "Service", required=True,
states={
'readonly': Eval('subscription_state') != 'draft',
},
2020-04-25 09:19:02 +02:00
context={
'company': Eval('company', None),
},
depends={'company'})
description = fields.Text("Description",
2017-04-02 00:40:05 +02:00
states={
'readonly': Eval('subscription_state') != 'draft',
})
2017-04-02 00:40:05 +02:00
quantity = fields.Float(
2021-08-30 00:27:13 +02:00
"Quantity", digits='unit',
2017-04-02 00:40:05 +02:00
states={
'readonly': Eval('subscription_state') != 'draft',
'required': Bool(Eval('consumption_recurrence')),
})
2017-04-02 00:40:05 +02:00
unit = fields.Many2One(
'product.uom', "Unit", required=True,
states={
'readonly': Eval('subscription_state') != 'draft',
},
domain=[
If(Bool(Eval('service_unit_category')),
('category', '=', Eval('service_unit_category')),
('category', '!=', -1)),
])
2017-04-02 00:40:05 +02:00
service_unit_category = fields.Function(
fields.Many2One('product.uom.category', "Service Unit Category"),
'on_change_with_service_unit_category')
2021-08-21 09:09:56 +02:00
unit_price = Monetary(
"Unit Price", currency='currency', digits=price_digits,
2017-04-02 00:40:05 +02:00
states={
'readonly': Eval('subscription_state') != 'draft',
})
2020-10-09 19:09:06 +02:00
currency = fields.Function(fields.Many2One('currency.currency',
'Currency'), 'on_change_with_currency')
2017-04-02 00:40:05 +02:00
consumption_recurrence = fields.Many2One(
'sale.subscription.recurrence.rule.set', "Consumption Recurrence",
states={
'readonly': Eval('subscription_state') != 'draft',
})
2017-04-02 00:40:05 +02:00
consumption_delay = fields.TimeDelta(
"Consumption Delay",
states={
'readonly': Eval('subscription_state') != 'draft',
'invisible': ~Eval('consumption_recurrence'),
})
2017-04-02 00:40:05 +02:00
next_consumption_date = fields.Date("Next Consumption Date", readonly=True)
next_consumption_date_delayed = fields.Function(
fields.Date("Next Consumption Delayed"),
'get_next_consumption_date_delayed')
consumed_until = fields.Date("Consumed until", readonly=True)
2017-04-02 00:40:05 +02:00
start_date = fields.Date(
2018-06-24 16:41:43 +02:00
"Start Date", required=True,
domain=[
2017-04-02 00:40:05 +02:00
('start_date', '>=', Eval('subscription_start_date')),
],
states={
'readonly': ((Eval('subscription_state') != 'draft')
| Eval('consumed_until')),
})
2017-04-02 00:40:05 +02:00
end_date = fields.Date(
"End Date",
2018-06-24 16:41:43 +02:00
domain=['OR', [
('end_date', '>=', Eval('start_date')),
2017-04-02 00:40:05 +02:00
If(Bool(Eval('subscription_end_date')),
('end_date', '<=', Eval('subscription_end_date')),
()),
If(Bool(Eval('consumed_until')),
('end_date', '>=', Eval('consumed_until')),
()),
2018-06-24 16:41:43 +02:00
],
2017-04-02 00:40:05 +02:00
('end_date', '=', None),
],
states={
'readonly': ((Eval('subscription_state') != 'draft')
| (~Eval('consumed_until') & Eval('consumed_until'))),
})
2017-04-02 00:40:05 +02:00
2021-03-07 11:13:53 +01:00
@classmethod
def __setup__(cls):
super().__setup__()
cls.__access__.add('subscription')
@classmethod
def __register__(cls, module):
2018-06-24 16:41:43 +02:00
transaction = Transaction()
cursor = transaction.connection.cursor()
table = cls.__table__()
super(Line, cls).__register__(module)
2018-07-24 22:01:08 +02:00
table_h = cls.__table_handler__(module)
# Migration from 5.2: replace consumed by consumed_until
if table_h.column_exist('consumed'):
cursor.execute(*table.update(
[table.consumed_until],
[Case((table.consumed, Coalesce(
table.next_consumption_date,
table.end_date)), else_=Null)]))
table_h.drop_column('consumed')
@classmethod
def get_subscription_states(cls):
pool = Pool()
Subscription = pool.get('sale.subscription')
return Subscription.fields_get(['state'])['state']['selection']
2017-04-02 00:40:05 +02:00
@fields.depends('subscription', '_parent_subscription.state')
def on_change_with_subscription_state(self, name=None):
if self.subscription:
return self.subscription.state
@fields.depends('subscription', '_parent_subscription.start_date')
def on_change_with_subscription_start_date(self, name=None):
if self.subscription:
return self.subscription.start_date
@fields.depends('subscription', '_parent_subscription.end_date')
def on_change_with_subscription_end_date(self, name=None):
if self.subscription:
return self.subscription.end_date
2020-04-25 09:19:02 +02:00
@fields.depends('subscription', '_parent_subscription.company')
def on_change_with_company(self, name=None):
2023-03-26 09:49:00 +02:00
return self.subscription.company if self.subscription else None
2020-04-25 09:19:02 +02:00
2018-06-24 16:41:43 +02:00
@fields.depends('subscription', 'start_date', 'end_date',
'_parent_subscription.start_date', '_parent_subscription.end_date')
def on_change_subscription(self):
if self.subscription:
if not self.start_date:
self.start_date = self.subscription.start_date
if not self.end_date:
self.end_date = self.subscription.end_date
2017-04-02 00:40:05 +02:00
@classmethod
def default_quantity(cls):
return 1
2020-10-09 19:09:06 +02:00
@fields.depends('subscription', '_parent_subscription.currency')
def on_change_with_currency(self, name=None):
2023-03-26 09:49:00 +02:00
return self.subscription.currency if self.subscription else None
2020-10-09 19:09:06 +02:00
2017-04-02 00:40:05 +02:00
@fields.depends('service')
def on_change_with_service_unit_category(self, name=None):
if self.service:
2023-03-26 09:49:00 +02:00
return self.service.product.default_uom_category
2017-04-02 00:40:05 +02:00
@fields.depends('service', 'quantity', 'unit',
2018-05-31 13:28:03 +02:00
'subscription', '_parent_subscription.party',
methods=['_get_context_sale_price'])
2017-04-02 00:40:05 +02:00
def on_change_service(self):
pool = Pool()
Product = pool.get('product.product')
if not self.service:
self.consumption_recurrence = None
self.consumption_delay = None
return
party = None
party_context = {}
if self.subscription and self.subscription.party:
party = self.subscription.party
if party.lang:
party_context['language'] = party.lang.code
product = self.service.product
category = product.sale_uom.category
if not self.unit or self.unit.category != category:
self.unit = product.sale_uom
with Transaction().set_context(self._get_context_sale_price()):
self.unit_price = Product.get_sale_price(
[product], self.quantity or 0)[product.id]
self.consumption_recurrence = self.service.consumption_recurrence
self.consumption_delay = self.service.consumption_delay
2018-05-31 13:28:03 +02:00
@fields.depends('subscription', '_parent_subscription.currency',
2020-04-25 09:19:02 +02:00
'_parent_subscription.party', 'start_date', 'unit', 'service',
'company')
2017-04-02 00:40:05 +02:00
def _get_context_sale_price(self):
context = {}
2018-05-31 13:28:03 +02:00
if self.subscription:
if self.subscription.currency:
2017-04-02 00:40:05 +02:00
context['currency'] = self.subscription.currency.id
2018-05-31 13:28:03 +02:00
if self.subscription.party:
2017-04-02 00:40:05 +02:00
context['customer'] = self.subscription.party.id
2018-06-24 16:41:43 +02:00
if self.start_date:
context['sale_date'] = self.start_date
2017-04-02 00:40:05 +02:00
if self.unit:
context['uom'] = self.unit.id
elif self.service:
context['uom'] = self.service.sale_uom.id
2020-04-25 09:19:02 +02:00
if self.company:
context['company'] = self.company.id
2017-04-02 00:40:05 +02:00
# TODO tax
return context
def get_next_consumption_date_delayed(self, name=None):
if self.next_consumption_date and self.consumption_delay:
return self.next_consumption_date + self.consumption_delay
return self.next_consumption_date
def get_rec_name(self, name):
2019-11-26 22:20:24 +01:00
pool = Pool()
Lang = pool.get('ir.lang')
lang = Lang.get()
return (lang.format_number_symbol(
self.quantity or 0, self.unit, digits=self.unit.digits)
+ ' %s @ %s' % (self.service.rec_name, self.subscription.rec_name))
@classmethod
def search_rec_name(cls, name, clause):
_, operator, value = clause
if operator.startswith('!') or operator.startswith('not '):
bool_op = 'AND'
else:
bool_op = 'OR'
return [bool_op,
('subscription.rec_name', *clause[1:]),
('service.rec_name', *clause[1:]),
]
2017-04-02 00:40:05 +02:00
@classmethod
def domain_next_consumption_date_delayed(cls, domain, tables):
field = cls.next_consumption_date_delayed._field
table, _ = tables[None]
name, operator, value = domain
Operator = fields.SQL_OPERATORS[operator]
column = (
table.next_consumption_date + Coalesce(
table.consumption_delay, datetime.timedelta()))
expression = Operator(column, field._domain_value(operator, value))
if isinstance(expression, operators.In) and not expression.right:
expression = Literal(False)
elif isinstance(expression, operators.NotIn) and not expression.right:
expression = Literal(True)
expression = field._domain_add_null(
column, operator, value, expression)
return expression
@classmethod
def generate_consumption(cls, date=None):
pool = Pool()
Date = pool.get('ir.date')
Consumption = pool.get('sale.subscription.line.consumption')
Subscription = pool.get('sale.subscription')
if date is None:
date = Date.today()
company_id = Transaction().context.get('company', -1)
2017-04-02 00:40:05 +02:00
remainings = all_lines = cls.search([
('consumption_recurrence', '!=', None),
('next_consumption_date_delayed', '<=', date),
('subscription.state', '=', 'running'),
('subscription.company', '=', company_id),
2017-04-02 00:40:05 +02:00
])
consumptions = []
subscription_ids = set()
while remainings:
lines, remainings = remainings, []
for line in lines:
consumption = line.get_consumption(line.next_consumption_date)
if consumption:
consumptions.append(consumption)
2017-04-02 00:40:05 +02:00
line.next_consumption_date = (
line.compute_next_consumption_date())
if line.next_consumption_date:
line.consumed_until = (
line.next_consumption_date - datetime.timedelta(1))
else:
line.consumed_until = line.end_date
2017-04-02 00:40:05 +02:00
if line.next_consumption_date is None:
subscription_ids.add(line.subscription.id)
elif line.get_next_consumption_date_delayed() <= date:
remainings.append(line)
Consumption.save(consumptions)
cls.save(all_lines)
Subscription.process(Subscription.browse(list(subscription_ids)))
def get_consumption(self, date):
pool = Pool()
Consumption = pool.get('sale.subscription.line.consumption')
end_date = self.end_date or self.subscription.end_date
if date < (end_date or datetime.date.max):
return Consumption(line=self, quantity=self.quantity, date=date)
2017-04-02 00:40:05 +02:00
def compute_next_consumption_date(self):
if not self.consumption_recurrence:
return None
2018-06-24 16:41:43 +02:00
date = self.next_consumption_date or self.start_date
rruleset = self.consumption_recurrence.rruleset(self.start_date)
2017-04-02 00:40:05 +02:00
dt = datetime.datetime.combine(date, datetime.time())
2018-06-24 16:41:43 +02:00
inc = (self.start_date == date) and not self.next_consumption_date
next_date = rruleset.after(dt, inc=inc)
if next_date:
next_date = next_date.date()
for end_date in [self.end_date, self.subscription.end_date]:
if end_date:
if next_date > end_date:
return None
return next_date
@classmethod
2022-04-11 23:24:21 +02:00
def validate_fields(cls, lines, field_names):
super().validate_fields(lines, field_names)
cls.validate_consumption_recurrence(lines, field_names)
2022-04-11 23:24:21 +02:00
@classmethod
def validate_consumption_recurrence(cls, lines, field_names=None):
if field_names and not (field_names & {
'consumption_recurrence', 'start_date'}):
return
for line in lines:
if line.consumption_recurrence:
try:
line.consumption_recurrence.rruleset(line.start_date)[0]
except IndexError:
raise InvalidRecurrence(gettext(
'sale_subscription'
'.msg_consumption_recurrence_invalid',
line=line.rec_name,
subscription=line.subscription.rec_name))
2017-04-02 00:40:05 +02:00
@classmethod
def copy(cls, lines, default=None):
if default is None:
default = {}
else:
default = default.copy()
default.setdefault('next_consumption_date', None)
2020-12-06 13:39:06 +01:00
default.setdefault('consumed_until', None)
2017-04-02 00:40:05 +02:00
return super(Line, cls).copy(lines, default=default)
class LineConsumption(ModelSQL, ModelView):
"Subscription Line Consumption"
__name__ = 'sale.subscription.line.consumption'
line = fields.Many2One(
'sale.subscription.line', "Line", required=True, ondelete='RESTRICT')
2021-08-30 00:27:13 +02:00
quantity = fields.Float("Quantity", digits='unit', required=True)
unit = fields.Function(fields.Many2One(
'product.uom', "Unit"), 'on_change_with_unit')
2017-04-02 00:40:05 +02:00
date = fields.Date("Date", required=True)
invoice_line = fields.Many2One(
'account.invoice.line', "Invoice Line", readonly=True)
@classmethod
def __setup__(cls):
super(LineConsumption, cls).__setup__()
2021-03-07 11:13:53 +01:00
cls.__access__.add('line')
2017-04-02 00:40:05 +02:00
cls._order.insert(0, ('date', 'DESC'))
@fields.depends('line')
2021-08-30 00:27:13 +02:00
def on_change_with_unit(self, name=None):
2023-03-26 09:49:00 +02:00
return self.line.unit if self.line else None
2017-04-02 00:40:05 +02:00
@classmethod
def copy(cls, consumptions, default=None):
if default is None:
default = {}
else:
default = default.copy()
2018-09-01 01:02:04 +02:00
default.setdefault('invoice_line', None)
2017-04-02 00:40:05 +02:00
return super(LineConsumption, cls).copy(consumptions, default=default)
@classmethod
def write(cls, *args):
2018-12-29 14:20:29 +01:00
for consumptions in args[::2]:
for consumption in consumptions:
if consumption.invoice_line:
raise AccessError(
gettext('sale_subscription'
'.msg_consumption_modify_invoiced',
consumption=consumption.rec_name))
2017-04-02 00:40:05 +02:00
super(LineConsumption, cls).write(*args)
@classmethod
def delete(cls, consumptions):
2018-12-29 14:20:29 +01:00
for consumption in consumptions:
if consumption.invoice_line:
raise AccessError(
gettext('sale_subscription'
'.msg_consumption_modify_invoiced',
consumption=consumption.rec_name))
2017-04-02 00:40:05 +02:00
super(LineConsumption, cls).delete(consumptions)
@classmethod
def get_invoice_lines(cls, consumptions, invoice):
"Return a list of lines and a list of consumptions"
pool = Pool()
InvoiceLine = pool.get('account.invoice.line')
lines, grouped_consumptions = [], []
2020-07-21 00:36:40 +02:00
consumptions = sorted(
consumptions, key=sortable_values(cls._group_invoice_key))
2017-04-02 00:40:05 +02:00
for key, sub_consumptions in groupby(
consumptions, key=cls._group_invoice_key):
sub_consumptions = list(sub_consumptions)
line = InvoiceLine(**dict(key))
line.invoice = invoice
line.on_change_invoice()
2017-04-02 00:40:05 +02:00
line.type = 'line'
line.quantity = sum(c.quantity for c in sub_consumptions)
line.on_change_product()
2017-04-02 00:40:05 +02:00
if not line.account:
2018-12-29 14:20:29 +01:00
raise InvoiceError(
gettext('sale_subscription'
'.msg_consumption_invoice_missing_account_revenue',
product=line.product.rec_name))
2017-04-02 00:40:05 +02:00
lines.append(line)
grouped_consumptions.append(sub_consumptions)
return lines, grouped_consumptions
@classmethod
def _group_invoice_key(cls, consumption):
return (
('company', consumption.line.subscription.company),
('currency', consumption.line.subscription.currency),
2017-04-02 00:40:05 +02:00
('unit', consumption.line.unit),
('product', consumption.line.service.product),
('unit_price', consumption.line.unit_price),
('description', consumption.line.description),
('origin', consumption.line),
)
class CreateLineConsumption(Wizard):
"Create Subscription Line Consumption"
__name__ = 'sale.subscription.line.consumption.create'
start = StateView(
'sale.subscription.line.consumption.create.start',
'sale_subscription.line_consumption_create_start_view_form', [
Button("Cancel", 'end', 'tryton-cancel'),
Button("Create", 'create_', 'tryton-ok', default=True),
])
create_ = StateAction(
'sale_subscription.act_subscription_line_consumption_form')
def do_create_(self, action):
pool = Pool()
Line = pool.get('sale.subscription.line')
Line.generate_consumption(date=self.start.date)
return action, {}
def transition_create_(self):
return 'end'
class CreateLineConsumptionStart(ModelView):
"Create Subscription Line Consumption"
__name__ = 'sale.subscription.line.consumption.create.start'
date = fields.Date("Date")
@classmethod
def default_date(cls):
pool = Pool()
Date = pool.get('ir.date')
return Date.today()
class CreateSubscriptionInvoice(Wizard):
"Create Subscription Invoice"
__name__ = 'sale.subscription.create_invoice'
start = StateView(
'sale.subscription.create_invoice.start',
'sale_subscription.create_invoice_start_view_form', [
Button("Cancel", 'end', 'tryton-cancel'),
Button("Create", 'create_', 'tryton-ok', default=True),
])
create_ = StateTransition()
def transition_create_(self):
pool = Pool()
Subscription = pool.get('sale.subscription')
Subscription.generate_invoice(date=self.start.date)
return 'end'
class CreateSubscriptionInvoiceStart(ModelView):
"Create Subscription Invoice"
__name__ = 'sale.subscription.create_invoice.start'
date = fields.Date("Date")
@classmethod
def default_date(cls):
pool = Pool()
Date = pool.get('ir.date')
return Date.today()