From 98b86f6a5e9c64430606b392dced31f57fe1fa55 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9dric=20Krier?= Date: Fri, 27 Jul 2018 09:27:49 +0200 Subject: [PATCH] Use task queue to process purchase It adds configured delay for processing purchase which provides a grace period. And it allows to reset to draft confirmed but not processed purchase. issue7463 review49401002 --- CHANGELOG | 2 + configuration.py | 4 + invoice.py | 18 ++-- purchase.py | 22 ++--- purchase.xml | 23 ----- stock.py | 140 ++++++++++++------------------ tests/scenario_purchase.rst | 11 --- tests/scenario_purchase_empty.rst | 3 - view/configuration_form.xml | 3 + 9 files changed, 84 insertions(+), 142 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 1c568faf95..83a505a92d 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,5 @@ +* Add configured delay for processing purchase +* Allow to reset to draft confirmed but not processed purchase * Show number and reference in purchase rec_name * Remove support for Python 2.7 * Invoice purchased product instead of received product diff --git a/configuration.py b/configuration.py index d20d618cb9..aaa3ac5221 100644 --- a/configuration.py +++ b/configuration.py @@ -37,6 +37,10 @@ class Configuration( ])) purchase_invoice_method = fields.MultiValue(purchase_invoice_method) get_purchase_invoice_method = get_purchase_methods('invoice_method') + purchase_process_after = fields.TimeDelta("Process Purchase after", + help="The grace period during which confirmed purchase " + "can still be reset to draft.\n" + "Applied only if a worker queue is activated.") @classmethod def multivalue_model(cls, field): diff --git a/invoice.py b/invoice.py index a95d0d9f59..53eb3a59e2 100644 --- a/invoice.py +++ b/invoice.py @@ -18,11 +18,11 @@ def process_purchase(func): pool = Pool() Purchase = pool.get('purchase.purchase') with Transaction().set_context(_check_access=False): - purchases = Purchase.browse( - set(p for i in cls.browse(invoices) for p in i.purchases)) + purchases = set( + p for i in cls.browse(invoices) for p in i.purchases) func(cls, invoices) - with Transaction().set_context(_check_access=False): - Purchase.process(purchases) + if purchases: + Purchase.__queue__.process(purchases) return wrapper @@ -155,11 +155,9 @@ class InvoiceLine(metaclass=PoolMeta): pool = Pool() Purchase = pool.get('purchase.purchase') with Transaction().set_context(_check_access=False): - lines = cls.browse(lines) - invoices = (l.invoice for l in lines + invoices = (l.invoice for l in cls.browse(lines) if l.type == 'line' and l.invoice) - purchases = Purchase.browse( - set(p for i in invoices for p in i.purchases)) + purchases = set(p for i in invoices for p in i.purchases) super(InvoiceLine, cls).delete(lines) - with Transaction().set_context(_check_access=False): - Purchase.process(purchases) + if purchases: + Purchase.__queue__.process(purchases) diff --git a/purchase.py b/purchase.py index 824dce7056..3e3151dac6 100644 --- a/purchase.py +++ b/purchase.py @@ -208,6 +208,7 @@ class Purchase(Workflow, ModelSQL, ModelView, TaxableMixin): ('draft', 'quotation'), ('quotation', 'confirmed'), ('confirmed', 'processing'), + ('confirmed', 'draft'), ('processing', 'processing'), ('processing', 'done'), ('done', 'processing'), @@ -222,7 +223,8 @@ class Purchase(Workflow, ModelSQL, ModelView, TaxableMixin): 'depends': ['state'], }, 'draft': { - 'invisible': ~Eval('state').in_(['cancel', 'quotation']), + 'invisible': ~Eval('state').in_( + ['cancel', 'quotation', 'confirmed']), 'icon': If(Eval('state') == 'cancel', 'tryton-clear', 'tryton-go-previous'), 'depends': ['state'], @@ -850,8 +852,15 @@ class Purchase(Workflow, ModelSQL, ModelView, TaxableMixin): @ModelView.button @Workflow.transition('confirmed') def confirm(cls, purchases): + pool = Pool() + Configuration = pool.get('purchase.configuration') cls.set_purchase_date(purchases) cls.store_cache(purchases) + config = Configuration(1) + with Transaction().set_context( + queue_name='purchase', + queue_scheduled_at=config.purchase_process_after): + cls.__queue__.process(purchases) @classmethod @Workflow.transition('processing') @@ -873,13 +882,6 @@ class Purchase(Workflow, ModelSQL, ModelView, TaxableMixin): def handle_shipment_exception(cls, purchases): pass - @classmethod - def cron_process(cls): - purchases = cls.search([ - ('state', '=', 'confirmed'), - ]) - cls.process(purchases) - @classmethod @ModelView.button def process(cls, purchases): @@ -1740,7 +1742,7 @@ class HandleShipmentException(Wizard): 'moves_recreated': [('add', moves_recreated)], }) - Purchase.process([purchase]) + Purchase.__queue__.process([purchase]) return 'end' @@ -1801,7 +1803,7 @@ class HandleInvoiceException(Wizard): 'invoices_recreated': [('add', invoices_recreated)], }) - Purchase.process([purchase]) + Purchase.__queue__.process([purchase]) return 'end' diff --git a/purchase.xml b/purchase.xml index 3cacb5cd86..08b5762339 100644 --- a/purchase.xml +++ b/purchase.xml @@ -361,29 +361,6 @@ this repository contains the full copyright notices and license terms. --> - - user_cron_process_purchase - Cron Process Purchase - - - - - - - - - - Process Confirmed Purchases - - - - hours - - purchase.purchase - cron_process - - purchase.line form diff --git a/stock.py b/stock.py index fdff0f13b8..90e2f80423 100644 --- a/stock.py +++ b/stock.py @@ -1,5 +1,7 @@ # This file is part of Tryton. The COPYRIGHT file at the top level of # this repository contains the full copyright notices and license terms. +from functools import wraps + from sql import Null from sql.aggregate import Max from sql.operators import Concat @@ -14,6 +16,22 @@ from trytond.modules.product import price_digits __all__ = ['ShipmentIn', 'ShipmentInReturn', 'Move', 'Location'] +def process_purchase(moves_field): + def _process_purchase(func): + @wraps(func) + def wrapper(cls, shipments): + pool = Pool() + Purchase = pool.get('purchase.purchase') + with Transaction().set_context(_check_access=False): + purchases = set(m.purchase for s in cls.browse(shipments) + for m in getattr(s, moves_field) if m.purchase) + func(cls, shipments) + if purchases: + Purchase.__queue__.process(purchases) + return wrapper + return _process_purchase + + class ShipmentIn(metaclass=PoolMeta): __name__ = 'stock.shipment.in' @@ -38,35 +56,6 @@ class ShipmentIn(metaclass=PoolMeta): 'it was generated by a purchase.'), }) - @classmethod - def write(cls, *args): - pool = Pool() - Purchase = pool.get('purchase.purchase') - PurchaseLine = pool.get('purchase.line') - - super(ShipmentIn, cls).write(*args) - - actions = iter(args) - for shipments, values in zip(actions, actions): - if values.get('state') not in ('received', 'cancel'): - continue - purchases = [] - move_ids = [] - for shipment in shipments: - move_ids.extend([x.id for x in shipment.incoming_moves]) - - purchase_lines = PurchaseLine.search([ - ('moves', 'in', move_ids), - ]) - if purchase_lines: - for purchase_line in purchase_lines: - if purchase_line.purchase not in purchases: - purchases.append(purchase_line.purchase) - - with Transaction().set_context(_check_access=False): - purchases = Purchase.browse([p.id for p in purchases]) - Purchase.process(purchases) - @classmethod @ModelView.button @Workflow.transition('draft') @@ -80,6 +69,20 @@ class ShipmentIn(metaclass=PoolMeta): return super(ShipmentIn, cls).draft(shipments) + @classmethod + @ModelView.button + @Workflow.transition('received') + @process_purchase('incoming_moves') + def receive(cls, shipments): + super(ShipmentIn, cls).receive(shipments) + + @classmethod + @ModelView.button + @Workflow.transition('cancel') + @process_purchase('incoming_moves') + def cancel(cls, shipments): + super(ShipmentIn, cls).cancel(shipments) + class ShipmentInReturn(metaclass=PoolMeta): __name__ = 'stock.shipment.in.return' @@ -121,34 +124,6 @@ class ShipmentInReturn(metaclass=PoolMeta): values=[value])) super(ShipmentInReturn, cls).__register__(module_name) - @classmethod - def write(cls, *args): - pool = Pool() - Purchase = pool.get('purchase.purchase') - PurchaseLine = pool.get('purchase.line') - - super(ShipmentInReturn, cls).write(*args) - - actions = iter(args) - for shipments, values in zip(actions, actions): - if values.get('state') != 'done': - continue - move_ids = [] - for shipment in shipments: - move_ids.extend([x.id for x in shipment.moves]) - - purchase_lines = PurchaseLine.search([ - ('moves', 'in', move_ids), - ]) - purchases = set() - if purchase_lines: - for purchase_line in purchase_lines: - purchases.add(purchase_line.purchase) - - with Transaction().set_context(_check_access=False): - purchases = Purchase.browse([p.id for p in purchases]) - Purchase.process(purchases) - @classmethod @ModelView.button @Workflow.transition('draft') @@ -162,6 +137,27 @@ class ShipmentInReturn(metaclass=PoolMeta): return super(ShipmentInReturn, cls).draft(shipments) + @classmethod + @ModelView.button + @Workflow.transition('done') + @process_purchase('moves') + def done(cls, shipments): + super(ShipmentInReturn, cls).done(shipments) + + +def process_purchase_move(func): + @wraps(func) + def wrapper(cls, moves): + pool = Pool() + Purchase = pool.get('purchase.purchase') + with Transaction().set_context(_check_access=False): + purchases = set( + m.purchase for m in cls.browse(moves) if m.purchase) + func(cls, moves) + if purchases: + Purchase.__queue__.process(purchases) + return wrapper + class Move(metaclass=PoolMeta): __name__ = 'stock.move' @@ -314,41 +310,15 @@ class Move(metaclass=PoolMeta): @classmethod @ModelView.button @Workflow.transition('cancel') + @process_purchase_move def cancel(cls, moves): - pool = Pool() - Purchase = pool.get('purchase.purchase') - PurchaseLine = pool.get('purchase.line') - super(Move, cls).cancel(moves) - purchase_lines = PurchaseLine.search([ - ('moves', 'in', [m.id for m in moves]), - ]) - if purchase_lines: - purchase_ids = list(set(l.purchase.id for l in purchase_lines)) - purchases = Purchase.browse(purchase_ids) - Purchase.process(purchases) @classmethod + @process_purchase_move def delete(cls, moves): - pool = Pool() - Purchase = pool.get('purchase.purchase') - PurchaseLine = pool.get('purchase.line') - - purchases = set() - purchase_lines = PurchaseLine.search([ - ('moves', 'in', [m.id for m in moves]), - ]) - super(Move, cls).delete(moves) - if purchase_lines: - for purchase_line in purchase_lines: - purchases.add(purchase_line.purchase) - if purchases: - with Transaction().set_context(_check_access=False): - purchases = Purchase.browse([p.id for p in purchases]) - Purchase.process(purchases) - class Location(metaclass=PoolMeta): __name__ = 'stock.location' diff --git a/tests/scenario_purchase.rst b/tests/scenario_purchase.rst index 197a742f35..598702f07c 100644 --- a/tests/scenario_purchase.rst +++ b/tests/scenario_purchase.rst @@ -153,9 +153,6 @@ Purchase 5 products:: >>> purchase.click('confirm') >>> purchase.untaxed_amount, purchase.tax_amount, purchase.total_amount (Decimal('25.00'), Decimal('2.50'), Decimal('27.50')) - >>> purchase.click('process') - >>> purchase.untaxed_amount, purchase.tax_amount, purchase.total_amount - (Decimal('25.00'), Decimal('2.50'), Decimal('27.50')) >>> purchase.state 'processing' >>> purchase.shipment_state @@ -215,7 +212,6 @@ Purchase 5 products with an invoice method 'on shipment':: >>> purchase_line.quantity = 3.0 >>> purchase.click('quote') >>> purchase.click('confirm') - >>> purchase.click('process') >>> purchase.state 'processing' >>> purchase.shipment_state @@ -309,7 +305,6 @@ Create a Return:: >>> return_line.description = 'Comment' >>> return_.click('quote') >>> return_.click('confirm') - >>> return_.click('process') >>> return_.state 'processing' >>> return_.shipment_state @@ -376,7 +371,6 @@ Mixing return and purchase:: >>> mixline2.quantity = -2. >>> mix.click('quote') >>> mix.click('confirm') - >>> mix.click('process') >>> mix.state 'processing' >>> mix.shipment_state @@ -445,7 +439,6 @@ Mixing stuff with an invoice method 'on shipment':: >>> mixline2.quantity = -3. >>> mix.click('quote') >>> mix.click('confirm') - >>> mix.click('process') >>> mix.state 'processing' >>> mix.shipment_state @@ -492,7 +485,6 @@ Purchase services:: >>> service_purchase.save() >>> service_purchase.click('quote') >>> service_purchase.click('confirm') - >>> service_purchase.click('process') >>> service_purchase.state 'processing' >>> service_purchase.shipment_state @@ -535,7 +527,6 @@ linked to invoices:: >>> line.quantity = 10.0 >>> purchase.click('quote') >>> purchase.click('confirm') - >>> purchase.click('process') >>> shipment = ShipmentIn() >>> shipment.supplier = supplier >>> for move in purchase.moves: @@ -570,7 +561,6 @@ correctly linked to invoices:: >>> line.quantity = 10.0 >>> purchase.click('quote') >>> purchase.click('confirm') - >>> purchase.click('process') >>> shipment = ShipmentIn() >>> shipment.supplier = supplier >>> for move in purchase.moves: @@ -607,7 +597,6 @@ Deleting a line from a invoice should recreate it:: >>> line.quantity = 10.0 >>> purchase.click('quote') >>> purchase.click('confirm') - >>> purchase.click('process') >>> invoice, = purchase.invoices >>> invoice_line, = invoice.lines >>> invoice.lines.remove(invoice_line) diff --git a/tests/scenario_purchase_empty.rst b/tests/scenario_purchase_empty.rst index 44345a2582..95392450d9 100644 --- a/tests/scenario_purchase_empty.rst +++ b/tests/scenario_purchase_empty.rst @@ -46,9 +46,6 @@ Create empty purchase:: Decimal('0') >>> purchase.click('confirm') >>> purchase.state - 'confirmed' - >>> purchase.click('process') - >>> purchase.state 'done' >>> purchase.shipment_state 'none' diff --git a/view/configuration_form.xml b/view/configuration_form.xml index c430726536..2871ee1c37 100644 --- a/view/configuration_form.xml +++ b/view/configuration_form.xml @@ -6,4 +6,7 @@ this repository contains the full copyright notices and license terms. -->