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. -->
+
+
+