From dd6b98bfcd02df424626cef268802d71b7b27329 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9dric=20Krier?= Date: Mon, 14 Oct 2019 00:17:45 +0200 Subject: [PATCH] Allow to finish line before next consumption issue8085 review72401002 --- CHANGELOG | 1 + subscription.py | 46 +++-- tests/scenario_sale_subscription_new_line.rst | 164 ++++++++++++++++++ tests/test_sale_subscription.py | 5 + view/subscription_line_form.xml | 14 +- 5 files changed, 209 insertions(+), 21 deletions(-) create mode 100644 tests/scenario_sale_subscription_new_line.rst 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. -->