summaryrefslogtreecommitdiff
path: root/addons
diff options
context:
space:
mode:
authorTiffany Chang (tic) <tic@odoo.com>2020-02-28 14:45:18 +0000
committerTiffany Chang (tic) <tic@odoo.com>2020-03-31 10:31:34 +0000
commit18e96c6be2cc1d74c98ad04bd7995c1f6ab73660 (patch)
tree9e3043e16550244dae142d1c3ee63faeacc0467e /addons
parentf7d7c53cd51c402672a49f98ddebab2cce7cb6ad (diff)
[IMP] purchase(_stock): Add more and update PO dates
Adds "Expected Date" and "Effective Date" fields to purchase orders to match what exists in sales orders. "Effective Date" mirrors sales logic (= first completed stock move). "Expected Date" has simplier logic due to simplier nature of receiving stock vs shipping (= earliest "Scheduled Date" of all PO lines). Form view has been updated to include these new fields. Additionally code was changed to not have PO 'order_line.date_planned' be overwritten by the 'purchase_order.date_planned' value. Previous behavior was both confusing and made it so info was lost. Also logic for related stock move picking line expected date values has been updated to now consider 'purchase_order.date_planned' value if set. Related changes include: - not overwriting when PO is edited+saved (this only occurred in write() logic, not create() which caused inconsistent behavior anyways.), - updating related tests, - removing PO line date_planned editing restriction when PO date_planned is set - updating of relevant stock moves/picking expected date due to PO.line.propogate_date = True ('purchase_order.date_planned' if exists, otherwise 'order_line.date_planned') This change also includes some small related improvements: - Removal of outdated code comment. - Addition of Help strings to better explain fields. - Prevent copy of 'date_planned' value into new POs (now mirrors sales logic). This change corresponds to "Receipt Date on PO's" subsection of Purchase KPIs task. Task #2198420
Diffstat (limited to 'addons')
-rw-r--r--addons/purchase/models/purchase.py29
-rw-r--r--addons/purchase/tests/test_purchase.py64
-rw-r--r--addons/purchase/views/purchase_views.xml7
-rw-r--r--addons/purchase_stock/models/purchase.py40
-rw-r--r--addons/purchase_stock/tests/test_purchase_lead_time.py1
-rw-r--r--addons/purchase_stock/views/purchase_views.xml7
6 files changed, 70 insertions, 78 deletions
diff --git a/addons/purchase/models/purchase.py b/addons/purchase/models/purchase.py
index dc52d85cae7..5b492df2691 100644
--- a/addons/purchase/models/purchase.py
+++ b/addons/purchase/models/purchase.py
@@ -96,10 +96,11 @@ class PurchaseOrder(models.Model):
('to invoice', 'Waiting Bills'),
('invoiced', 'Fully Billed'),
], string='Billing Status', compute='_get_invoiced', store=True, readonly=True, copy=False, default='no')
-
- # There is no inverse function on purpose since the date may be different on each line
- date_planned = fields.Datetime(string='Receipt Date', index=True,
- help='This is the Receipt Date promised by the supplier. If set, the receipt will be scheduled at this date.')
+ date_planned = fields.Datetime(
+ string='Receipt Date', index=True, copy=False,
+ help="Delivery date promised by vendor. This date is used to determine expected arrival of products.")
+ expected_date = fields.Datetime("Expected Date", compute='_compute_expected_date', store=True,
+ help="Delivery date expected by vendor, computed from the minimum lead time of the order lines.")
date_calendar_start = fields.Datetime(compute='_compute_date_calendar_start', readonly=True, store=True)
amount_untaxed = fields.Monetary(string='Untaxed Amount', store=True, readonly=True, compute='_amount_all', tracking=True)
@@ -149,6 +150,13 @@ class PurchaseOrder(models.Model):
for order in self:
order.currency_rate = self.env['res.currency']._get_conversion_rate(order.company_id.currency_id, order.currency_id, order.company_id, order.date_order)
+ @api.depends('order_line.date_planned')
+ def _compute_expected_date(self):
+ """ expected_date = the earliest date_planned across all order lines. """
+ for order in self:
+ dates_list = order.order_line.mapped('date_planned')
+ order.expected_date = fields.Datetime.to_string(min(dates_list)) if dates_list else False
+
@api.depends('name', 'partner_ref')
def name_get(self):
result = []
@@ -170,12 +178,6 @@ class PurchaseOrder(models.Model):
vals['name'] = self.env['ir.sequence'].next_by_code('purchase.order', sequence_date=seq_date) or '/'
return super(PurchaseOrder, self).create(vals)
- def write(self, vals):
- res = super(PurchaseOrder, self).write(vals)
- if vals.get('date_planned'):
- self.order_line.filtered(lambda line: not line.display_type).date_planned = vals['date_planned']
- return res
-
def unlink(self):
for order in self:
if not order.state == 'cancel':
@@ -188,9 +190,7 @@ class PurchaseOrder(models.Model):
self = self.with_context(ctx)
new_po = super(PurchaseOrder, self).copy(default=default)
for line in new_po.order_line:
- if new_po.date_planned:
- line.date_planned = new_po.date_planned
- elif line.product_id:
+ if line.product_id:
seller = line.product_id._select_seller(
partner_id=line.partner_id, quantity=line.product_qty,
date=line.order_id.date_order and line.order_id.date_order.date(), uom_id=line.product_uom)
@@ -539,7 +539,8 @@ class PurchaseOrderLine(models.Model):
sequence = fields.Integer(string='Sequence', default=10)
product_qty = fields.Float(string='Quantity', digits='Product Unit of Measure', required=True)
product_uom_qty = fields.Float(string='Total Quantity', compute='_compute_product_uom_qty', store=True)
- date_planned = fields.Datetime(string='Scheduled Date', index=True)
+ date_planned = fields.Datetime(string='Scheduled Date', index=True,
+ help="Delivery date expected from vendor. This date respectively defaults to vendor pricelist lead time then today's date.")
taxes_id = fields.Many2many('account.tax', string='Taxes', domain=['|', ('active', '=', False), ('active', '=', True)])
product_uom = fields.Many2one('uom.uom', string='Unit of Measure', domain="[('category_id', '=', product_uom_category_id)]")
product_uom_category_id = fields.Many2one(related='product_id.uom_id.category_id')
diff --git a/addons/purchase/tests/test_purchase.py b/addons/purchase/tests/test_purchase.py
index 996eefa6640..d25436d5562 100644
--- a/addons/purchase/tests/test_purchase.py
+++ b/addons/purchase/tests/test_purchase.py
@@ -22,11 +22,10 @@ class TestPurchase(SavepointCase):
cls.vendor = cls.env['res.partner'].create({'name': 'vendor1'})
cls.uom_unit = cls.env.ref('uom.product_uom_unit')
- def test_date_planned_1(self):
- """Set a date planned on a PO, see that it is set on the PO lines. Try to edit the date
- planned of the PO line, see that it is not possible. Unset the date planned on the PO and
- edit the date planned on the PO lines. Validate the PO and see that it isn't possible to
- set the date planned on the PO nor on the PO lines.
+ def test_expected_date(self):
+ """Set a date planned on 2 PO lines. Check that the PO expected date is the earliest PO line date
+ planned. Change one of the dates so it is even earlier and check that the expected date is set to
+ this earlier date.
"""
po = Form(self.env['purchase.order'])
po.partner_id = self.vendor
@@ -40,57 +39,22 @@ class TestPurchase(SavepointCase):
po_line.price_unit = 200
po = po.save()
- # Check there is no date planned on the PO and the same date planned on both PO lines.
- self.assertEqual(po.date_planned, False)
+ # Check that the same date is planned on both PO lines.
self.assertNotEqual(po.order_line[0].date_planned, False)
self.assertAlmostEqual(po.order_line[0].date_planned, po.order_line[1].date_planned, delta=timedelta(seconds=10))
+ self.assertAlmostEqual(po.order_line[0].date_planned, po.expected_date, delta=timedelta(seconds=10))
orig_date_planned = po.order_line[0].date_planned
- # Set a date planned on a PO, see that it is set on the PO lines.
- new_date_planned = orig_date_planned + timedelta(hours=1)
- po.date_planned = new_date_planned
- self.assertAlmostEqual(po.order_line[0].date_planned, new_date_planned, delta=timedelta(seconds=10))
- self.assertAlmostEqual(po.order_line[1].date_planned, new_date_planned, delta=timedelta(seconds=10))
+ # Set an earlier date planned on a PO line and check that the PO expected date matches it.
+ new_date_planned = orig_date_planned - timedelta(hours=1)
+ po.order_line[0].date_planned = new_date_planned
+ self.assertAlmostEqual(po.order_line[0].date_planned, po.expected_date, delta=timedelta(seconds=10))
- # Try to edit the date planned of the PO line, see that it is not possible
- po = Form(po)
- with self.assertRaises(AssertionError):
- po.order_line.edit(0).date_planned = orig_date_planned
- with self.assertRaises(AssertionError):
- po.order_line.edit(1).date_planned = orig_date_planned
- po = po.save()
-
- self.assertAlmostEqual(po.order_line[0].date_planned, new_date_planned, delta=timedelta(seconds=10))
- self.assertAlmostEqual(po.order_line[1].date_planned, new_date_planned, delta=timedelta(seconds=10))
-
- # Unset the date planned on the PO and edit the date planned on the PO line.
- po = Form(po)
- po.date_planned = False
- with po.order_line.edit(0) as po_line:
- po_line.date_planned = orig_date_planned
- with po.order_line.edit(1) as po_line:
- po_line.date_planned = orig_date_planned
- po = po.save()
-
- self.assertAlmostEqual(po.order_line[0].date_planned, orig_date_planned, delta=timedelta(seconds=10))
- self.assertAlmostEqual(po.order_line[1].date_planned, orig_date_planned, delta=timedelta(seconds=10))
-
- # Validate the PO and see that it isn't possible to set the date planned on the PO
- # nor on the PO lines.
- po.button_confirm()
- po.button_done()
-
- po = Form(po)
- with self.assertRaises(AssertionError):
- po.date_planned = new_date_planned
- with self.assertRaises(AssertionError):
- with po.order_line.edit(0) as po_line:
- po_line.date_planned = orig_date_planned
- with self.assertRaises(AssertionError):
- with po.order_line.edit(1) as po_line:
- po_line.date_planned = orig_date_planned
- po.save()
+ # Set an even earlier date planned on the other PO line and check that the PO expected date matches it.
+ new_date_planned = orig_date_planned - timedelta(hours=72)
+ po.order_line[1].date_planned = new_date_planned
+ self.assertAlmostEqual(po.order_line[1].date_planned, po.expected_date, delta=timedelta(seconds=10))
def test_purchase_order_sequence(self):
PurchaseOrder = self.env['purchase.order'].with_context(tracking_disable=True)
diff --git a/addons/purchase/views/purchase_views.xml b/addons/purchase/views/purchase_views.xml
index da5d47989b4..7c2ddb8e1a3 100644
--- a/addons/purchase/views/purchase_views.xml
+++ b/addons/purchase/views/purchase_views.xml
@@ -209,7 +209,7 @@
context="{'partner_id':parent.partner_id, 'quantity':product_qty,'uom':product_uom, 'company_id': parent.company_id}"
force_save="1" domain="[('purchase_ok', '=', True), '|', ('company_id', '=', False), ('company_id', '=', parent.company_id)]"/>
<field name="name" widget="section_and_note_text"/>
- <field name="date_planned" optional="hide" attrs="{'required': [('display_type', '=', False)], 'readonly': [('parent.date_planned', '!=', False)]}"/>
+ <field name="date_planned" optional="hide" attrs="{'required': [('display_type', '=', False)]}"/>
<field name="account_analytic_id" optional="hide" context="{'default_partner_id':parent.partner_id}" groups="analytic.group_analytic_accounting" domain="['|', ('company_id', '=', False), ('company_id', '=', parent.company_id)]"/>
<field name="analytic_tag_ids" optional="hide" groups="analytic.group_analytic_tags" widget="many2many_tags" options="{'color_field': 'color'}" domain="['|', ('company_id', '=', False), ('company_id', '=', parent.company_id)]"/>
<field name="product_qty"/>
@@ -342,8 +342,9 @@
<group>
<group name="planning">
<label for="date_planned"/>
- <div>
- <field name="date_planned" attrs="{'readonly': [('state', 'not in', ('draft', 'sent'))]}"/>
+ <div name="date_planned_div" class="o_row">
+ <field name="date_planned" attrs="{'readonly': [('state', 'not in', ('draft', 'sent', 'to approve', 'purchase'))]}"/>
+ <span class="text-muted">Expected: <field name="expected_date" widget="date"/></span>
</div>
</group>
<group name="other_info">
diff --git a/addons/purchase_stock/models/purchase.py b/addons/purchase_stock/models/purchase.py
index 7bca1d03090..229af1728f5 100644
--- a/addons/purchase_stock/models/purchase.py
+++ b/addons/purchase_stock/models/purchase.py
@@ -27,6 +27,7 @@ class PurchaseOrder(models.Model):
help="Technical field used to display the Drop Ship Address", readonly=True)
group_id = fields.Many2one('procurement.group', string="Procurement Group", copy=False)
is_shipped = fields.Boolean(compute="_compute_is_shipped")
+ effective_date = fields.Date("Effective Date", compute='_compute_effective_date', help="Completion date of the first receipt order.")
@api.depends('order_line.move_ids.returned_move_ids',
'order_line.move_ids.state',
@@ -42,6 +43,13 @@ class PurchaseOrder(models.Model):
order.picking_ids = pickings
order.picking_count = len(pickings)
+ @api.depends('picking_ids.date_done')
+ def _compute_effective_date(self):
+ for order in self:
+ pickings = order.picking_ids.filtered(lambda x: x.state == 'done' and x.location_dest_id.usage == 'internal' and x.date_done)
+ dates_list = pickings.mapped('date_done')
+ order.effective_date = min(dates_list).date() if dates_list else False
+
@api.depends('picking_ids', 'picking_ids.state')
def _compute_is_shipped(self):
for order in self:
@@ -78,6 +86,14 @@ class PurchaseOrder(models.Model):
to_log[order_line] = (order_line.product_qty, pre_order_line_qty[order_line])
if to_log:
order._log_decrease_ordered_quantity(to_log)
+ # if date_planned value is set, use this to set move line expected dates
+ if vals.get('date_planned'):
+ new_date = fields.Datetime.to_datetime(vals['date_planned'])
+ # let's make sure it's not "no date"
+ if new_date:
+ order_lines_to_update = self.order_line.filtered(lambda l: l.propagate_date)
+ for order_line in order_lines_to_update:
+ order_line._update_move_expected_date(new_date)
return res
# --------------------------------------------------
@@ -287,16 +303,10 @@ class PurchaseOrderLine(models.Model):
def write(self, values):
for line in self.filtered(lambda l: not l.display_type):
- if values.get('date_planned') and line.propagate_date:
+ # PO date_planned overrides any PO line date_planned values
+ if not line.order_id.date_planned and values.get('date_planned') and line.propagate_date:
new_date = fields.Datetime.to_datetime(values['date_planned'])
- delta_days = (new_date - line.date_planned).total_seconds() / 86400
- if abs(delta_days) < line.propagate_date_minimum_delta:
- continue
- moves_to_update = line.move_ids.filtered(lambda m: m.state not in ('done', 'cancel'))
- if not moves_to_update:
- moves_to_update = line.move_dest_ids.filtered(lambda m: m.state not in ('done', 'cancel'))
- for move in moves_to_update:
- move.date_expected = move.date_expected + relativedelta.relativedelta(days=delta_days)
+ self._update_move_expected_date(new_date)
result = super(PurchaseOrderLine, self).write(values)
if 'product_qty' in values:
self.filtered(lambda l: l.order_id.state == 'purchase')._create_or_update_picking()
@@ -306,6 +316,16 @@ class PurchaseOrderLine(models.Model):
# Business methods
# --------------------------------------------------
+ def _update_move_expected_date(self, new_date):
+ """ Updates corresponding move picking line expected dates that are not yet completed. """
+ moves_to_update = self.move_ids.filtered(lambda m: m.state not in ('done', 'cancel'))
+ if not moves_to_update:
+ moves_to_update = self.move_dest_ids.filtered(lambda m: m.state not in ('done', 'cancel'))
+ for move in moves_to_update:
+ delta_days = (new_date - move.date_expected).total_seconds() / 86400
+ if abs(delta_days) >= self.propagate_date_minimum_delta:
+ move.date_expected = move.date_expected + relativedelta.relativedelta(days=delta_days)
+
def _create_or_update_picking(self):
for line in self:
if line.product_id and line.product_id.type in ('product', 'consu'):
@@ -396,7 +416,7 @@ class PurchaseOrderLine(models.Model):
'name': (self.name or '')[:2000],
'product_id': self.product_id.id,
'date': self.order_id.date_order,
- 'date_expected': self.date_planned,
+ 'date_expected': self.order_id.date_planned or self.date_planned,
'location_id': self.order_id.partner_id.property_stock_supplier.id,
'location_dest_id': (self.orderpoint_id and not (self.move_ids | self.move_dest_ids)) and self.orderpoint_id.location_id.id or self.order_id._get_destination_location(),
'picking_id': picking.id,
diff --git a/addons/purchase_stock/tests/test_purchase_lead_time.py b/addons/purchase_stock/tests/test_purchase_lead_time.py
index fd8f81ac201..a6026a511f2 100644
--- a/addons/purchase_stock/tests/test_purchase_lead_time.py
+++ b/addons/purchase_stock/tests/test_purchase_lead_time.py
@@ -24,7 +24,6 @@ class TestPurchaseLeadTime(PurchaseTestCommon):
date_planned = fields.Datetime.to_string(fields.datetime.now() + timedelta(days=10))
self._create_make_procurement(self.product_1, 15.00, date_planned=date_planned)
purchase = self.env['purchase.order.line'].search([('product_id', '=', self.product_1.id)], limit=1).order_id
-
# Confirm purchase order
purchase.button_confirm()
diff --git a/addons/purchase_stock/views/purchase_views.xml b/addons/purchase_stock/views/purchase_views.xml
index f42a82c73f2..942cdcbf688 100644
--- a/addons/purchase_stock/views/purchase_views.xml
+++ b/addons/purchase_stock/views/purchase_views.xml
@@ -24,6 +24,13 @@
<xpath expr="//field[@name='order_line']/tree//field[@name='date_planned']" position="after">
<field name="move_dest_ids" invisible="1"/>
</xpath>
+ <xpath expr="//div[@name='date_planned_div']" position="replace">
+ <div class="o_row">
+ <field name="date_planned" attrs="{'readonly': [('state', 'not in', ('draft', 'sent', 'to approve', 'purchase'))]}"/>
+ <span class="text-muted" attrs="{'invisible': [('effective_date', '!=', False), ('date_planned', '!=', False)]}">Expected: <field name="expected_date" widget="date"/></span>
+ </div>
+ <field name="effective_date" attrs="{'invisible': [('effective_date', '=', False)]}"/>
+ </xpath>
<xpath expr="//field[@name='order_line']/form//field[@name='invoice_lines']" position="after">
<field name="move_ids"/>
</xpath>