diff --git a/CHANGELOG b/CHANGELOG
index 18adb1b202..5fa2e6d2bc 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -1,3 +1,4 @@
+* Allow lines to be finished before next consumption
* Allow services to be deactivated
Version 5.2.0 - 2019-05-06
diff --git a/subscription.py b/subscription.py
index 130fc7dfdc..c4f6143da6 100644
--- a/subscription.py
+++ b/subscription.py
@@ -5,7 +5,7 @@ from decimal import Decimal
from itertools import groupby
from sql import operators, Literal, Null
-from sql.conditionals import Coalesce
+from sql.conditionals import Coalesce, Case
from trytond import backend
from trytond.i18n import gettext
@@ -279,7 +279,7 @@ class Subscription(Workflow, ModelSQL, ModelView):
subscription.compute_next_invoice_date())
for line in subscription.lines:
if (line.next_consumption_date is None
- and not line.consumed):
+ and not line.consumed_until):
line.next_consumption_date = (
line.compute_next_consumption_date())
lines.extend(subscription.lines)
@@ -465,7 +465,7 @@ class Line(sequence_ordered(), ModelSQL, ModelView):
next_consumption_date_delayed = fields.Function(
fields.Date("Next Consumption Delayed"),
'get_next_consumption_date_delayed')
- consumed = fields.Boolean("Consumed")
+ consumed_until = fields.Date("Consumed until", readonly=True)
start_date = fields.Date(
"Start Date", required=True,
domain=[
@@ -473,9 +473,10 @@ class Line(sequence_ordered(), ModelSQL, ModelView):
],
states={
'readonly': ((Eval('subscription_state') != 'draft')
- | Eval('consumed')),
+ | Eval('consumed_until')),
},
- depends=['subscription_start_date', 'subscription_state', 'consumed'])
+ depends=[
+ 'subscription_start_date', 'subscription_state', 'consumed_until'])
end_date = fields.Date(
"End Date",
domain=['OR', [
@@ -483,18 +484,18 @@ class Line(sequence_ordered(), ModelSQL, ModelView):
If(Bool(Eval('subscription_end_date')),
('end_date', '<=', Eval('subscription_end_date')),
()),
- If(Bool(Eval('next_consumption_date')),
- ('end_date', '>=', Eval('next_consumption_date')),
+ If(Bool(Eval('consumed_until')),
+ ('end_date', '>=', Eval('consumed_until')),
()),
],
('end_date', '=', None),
],
states={
'readonly': ((Eval('subscription_state') != 'draft')
- | (~Eval('next_consumption_date') & Eval('consumed'))),
+ | (~Eval('consumed_until') & Eval('consumed_until'))),
},
depends=['subscription_end_date', 'start_date',
- 'next_consumption_date', 'subscription_state', 'consumed'])
+ 'consumed_until', 'subscription_state', 'consumed_until'])
@classmethod
def __register__(cls, module):
@@ -523,6 +524,15 @@ class Line(sequence_ordered(), ModelSQL, ModelView):
# Migration from 4.8: drop required on description
table_h.not_null_action('description', action='remove')
+ # 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')
+
@fields.depends('subscription', '_parent_subscription.state')
def on_change_with_subscription_state(self, name=None):
if self.subscription:
@@ -620,10 +630,6 @@ class Line(sequence_ordered(), ModelSQL, ModelView):
return self.next_consumption_date + self.consumption_delay
return self.next_consumption_date
- @classmethod
- def default_consumed(cls):
- return False
-
def get_rec_name(self, name):
return '%s @ %s' % (self.service.rec_name, self.subscription.rec_name)
@@ -675,11 +681,16 @@ class Line(sequence_ordered(), ModelSQL, ModelView):
while remainings:
lines, remainings = remainings, []
for line in lines:
- consumptions.append(
- line.get_consumption(line.next_consumption_date))
+ consumption = line.get_consumption(line.next_consumption_date)
+ if consumption:
+ consumptions.append(consumption)
line.next_consumption_date = (
line.compute_next_consumption_date())
- line.consumed = True
+ if line.next_consumption_date:
+ line.consumed_until = (
+ line.next_consumption_date - datetime.timedelta(1))
+ else:
+ line.consumed_until = line.end_date
if line.next_consumption_date is None:
subscription_ids.add(line.subscription.id)
elif line.get_next_consumption_date_delayed() <= date:
@@ -692,7 +703,8 @@ class Line(sequence_ordered(), ModelSQL, ModelView):
def get_consumption(self, date):
pool = Pool()
Consumption = pool.get('sale.subscription.line.consumption')
- return Consumption(line=self, quantity=self.quantity, date=date)
+ if date < (self.end_date or datetime.date.max):
+ return Consumption(line=self, quantity=self.quantity, date=date)
def compute_next_consumption_date(self):
if not self.consumption_recurrence:
diff --git a/tests/scenario_sale_subscription_new_line.rst b/tests/scenario_sale_subscription_new_line.rst
new file mode 100644
index 0000000000..bf4d000bbe
--- /dev/null
+++ b/tests/scenario_sale_subscription_new_line.rst
@@ -0,0 +1,164 @@
+===================================
+Sale Subscription New Line Scenario
+===================================
+
+Imports::
+
+ >>> import datetime
+ >>> from decimal import Decimal
+ >>> from dateutil.relativedelta import relativedelta
+ >>> from proteus import Model, Wizard
+ >>> from trytond.tests.tools import activate_modules
+ >>> from trytond.modules.company.tests.tools import create_company, \
+ ... get_company
+ >>> from trytond.modules.account.tests.tools import create_chart, \
+ ... get_accounts
+
+Install sale_subscription::
+
+ >>> config = activate_modules('sale_subscription')
+
+Create company::
+
+ >>> _ = create_company()
+ >>> company = get_company()
+
+Create chart of accounts::
+
+ >>> _ = create_chart(company)
+ >>> accounts = get_accounts(company)
+ >>> revenue = accounts['revenue']
+
+Create party::
+
+ >>> Party = Model.get('party.party')
+
+ >>> customer = Party(name='Customer')
+ >>> customer.save()
+
+Create subscription recurrence rule sets::
+
+ >>> RecurrenceRuleSet = Model.get('sale.subscription.recurrence.rule.set')
+
+ >>> monthly = RecurrenceRuleSet(name='Monthly')
+ >>> rule, = monthly.rules
+ >>> rule.freq = 'monthly'
+ >>> rule.interval = 1
+ >>> monthly.save()
+
+Create account category::
+
+ >>> ProductCategory = Model.get('product.category')
+ >>> account_category = ProductCategory(name="Account Category")
+ >>> account_category.accounting = True
+ >>> account_category.account_revenue = revenue
+ >>> account_category.save()
+
+Create subscription service::
+
+ >>> Service = Model.get('sale.subscription.service')
+ >>> ProductTemplate = Model.get('product.template')
+ >>> Uom = Model.get('product.uom')
+
+ >>> unit, = Uom.find([('name', '=', 'Unit')])
+
+ >>> template = ProductTemplate()
+ >>> template.name = 'Rental'
+ >>> template.default_uom = unit
+ >>> template.type = 'service'
+ >>> template.list_price = Decimal('10')
+ >>> template.account_category = account_category
+ >>> template.save()
+ >>> product, = template.products
+
+ >>> service = Service()
+ >>> service.product = product
+ >>> service.consumption_recurrence = monthly
+ >>> service.save()
+
+Subscribe::
+
+ >>> Subscription = Model.get('sale.subscription')
+
+ >>> subscription = Subscription()
+ >>> subscription.party = customer
+ >>> subscription.start_date = datetime.date(2019, 1, 1)
+ >>> subscription.invoice_recurrence = monthly
+ >>> line = subscription.lines.new()
+ >>> line.service = service
+ >>> line.quantity = 1
+ >>> subscription.click('quote')
+ >>> subscription.state
+ 'quotation'
+ >>> subscription.click('run')
+ >>> subscription.state
+ 'running'
+
+Create consumption for next two months::
+
+ >>> LineConsumption = Model.get('sale.subscription.line.consumption')
+
+ >>> line_consumption_create = Wizard(
+ ... 'sale.subscription.line.consumption.create')
+ >>> line_consumption_create.form.date = datetime.date(2019, 2, 1)
+ >>> line_consumption_create.execute('create_')
+
+ >>> len(LineConsumption.find([]))
+ 2
+
+Create invoice for next two months::
+
+ >>> Invoice = Model.get('account.invoice')
+
+ >>> create_invoice = Wizard('sale.subscription.create_invoice')
+ >>> create_invoice.form.date = datetime.date(2019, 2, 1)
+ >>> create_invoice.execute('create_')
+
+ >>> invoice, = Invoice.find([])
+ >>> line, = invoice.lines
+ >>> line.quantity
+ 2.0
+ >>> line.unit_price
+ Decimal('10.0000')
+
+Close subscription::
+
+ >>> subscription.click('draft')
+ >>> subscription.state
+ 'draft'
+ >>> line, = subscription.lines
+ >>> line.consumed_until
+ datetime.date(2019, 2, 28)
+ >>> line.end_date = datetime.date(2019, 2, 28)
+ >>> new_line = subscription.lines.new()
+ >>> new_line.service = service
+ >>> new_line.quantity = 1
+ >>> new_line.start_date = datetime.date(2019, 3, 1)
+ >>> new_line.unit_price = Decimal('15.00')
+ >>> subscription.click('quote')
+ >>> subscription.click('run')
+ >>> subscription.state
+ 'running'
+
+ >>> line_consumption_create = Wizard(
+ ... 'sale.subscription.line.consumption.create')
+ >>> line_consumption_create.form.date = datetime.date(2019, 3, 1)
+ >>> line_consumption_create.execute('create_')
+
+ >>> len(LineConsumption.find([]))
+ 3
+
+Create next invoice::
+
+ >>> Invoice = Model.get('account.invoice')
+
+ >>> create_invoice = Wizard('sale.subscription.create_invoice')
+ >>> create_invoice.form.date = datetime.date(2019, 3, 1)
+ >>> create_invoice.execute('create_')
+
+ >>> new_invoice, = Invoice.find([('id', '!=', invoice.id)])
+ >>> line, = new_invoice.lines
+ >>> line.quantity
+ 1.0
+ >>> line.unit_price
+ Decimal('15.00')
diff --git a/tests/test_sale_subscription.py b/tests/test_sale_subscription.py
index a40aa514ff..0dd4370dbb 100644
--- a/tests/test_sale_subscription.py
+++ b/tests/test_sale_subscription.py
@@ -25,4 +25,9 @@ def suite():
tearDown=doctest_teardown, encoding='utf-8',
checker=doctest_checker,
optionflags=doctest.REPORT_ONLY_FIRST_FAILURE))
+ suite.addTests(doctest.DocFileSuite(
+ 'scenario_sale_subscription_new_line.rst',
+ tearDown=doctest_teardown, encoding='utf-8',
+ checker=doctest_checker,
+ optionflags=doctest.REPORT_ONLY_FIRST_FAILURE))
return suite
diff --git a/view/subscription_line_form.xml b/view/subscription_line_form.xml
index 942c7296a3..5ebbed532a 100644
--- a/view/subscription_line_form.xml
+++ b/view/subscription_line_form.xml
@@ -6,28 +6,34 @@ this repository contains the full copyright notices and license terms. -->
+
-
+
+
+
+
+
-
-
-
+
+
+
+