@@ -1,3 +1,4 @@
|
||||
* Calculate the average lead time for product suppliers
|
||||
|
||||
Version 8.0.0 - 2026-04-20
|
||||
--------------------------
|
||||
|
||||
+101
-11
@@ -3,8 +3,13 @@
|
||||
import datetime
|
||||
from collections import defaultdict
|
||||
|
||||
from sql.aggregate import Count, Max
|
||||
from dateutil.relativedelta import relativedelta
|
||||
from sql import Null
|
||||
from sql.aggregate import Avg, Count, Max
|
||||
from sql.conditionals import Case, Coalesce
|
||||
from sql.functions import Age, DateTrunc
|
||||
|
||||
from trytond import backend
|
||||
from trytond.i18n import gettext
|
||||
from trytond.model import (
|
||||
Index, MatchMixin, ModelSQL, ModelView, fields, sequence_ordered)
|
||||
@@ -14,7 +19,7 @@ from trytond.modules.product import (
|
||||
price_digits, round_price)
|
||||
from trytond.pool import Pool, PoolMeta
|
||||
from trytond.pyson import Bool, Eval, If, TimeDelta
|
||||
from trytond.tools import is_full_text, lstrip_wildcard
|
||||
from trytond.tools import is_full_text, lstrip_wildcard, sqlite_apply_types
|
||||
from trytond.transaction import Transaction
|
||||
|
||||
from .exceptions import PurchaseUOMWarning
|
||||
@@ -313,6 +318,12 @@ class ProductSupplier(
|
||||
help="The time from confirming the purchase order to receiving the "
|
||||
"products.\n"
|
||||
"If empty the lead time of the supplier is used.")
|
||||
lead_time_used = fields.Function(
|
||||
fields.TimeDelta("Lead Time"),
|
||||
'on_change_with_lead_time_used')
|
||||
average_lead_time = fields.Function(
|
||||
fields.TimeDelta("Average Lead Time"),
|
||||
'get_average_lead_time')
|
||||
currency = fields.Many2One('currency.currency', 'Currency', required=True,
|
||||
ondelete='RESTRICT')
|
||||
unit = fields.Function(
|
||||
@@ -358,6 +369,78 @@ class ProductSupplier(
|
||||
if row:
|
||||
self.currency, = row
|
||||
|
||||
@classmethod
|
||||
def get_average_lead_time(cls, product_suppliers, name):
|
||||
return defaultdict(
|
||||
lambda: None, cls._average_lead_time(product_suppliers))
|
||||
|
||||
@classmethod
|
||||
def _average_lead_time(cls, product_suppliers, delta=None, date=None):
|
||||
pool = Pool()
|
||||
Date = pool.get('ir.date')
|
||||
Purchase = pool.get('purchase.purchase')
|
||||
PurchaseLine = pool.get('purchase.line')
|
||||
Product = pool.get('product.product')
|
||||
Move = pool.get('stock.move')
|
||||
table = cls.__table__()
|
||||
cursor = Transaction().connection.cursor()
|
||||
|
||||
purchase = Purchase.__table__()
|
||||
purchase_line = PurchaseLine.__table__()
|
||||
product = Product.__table__()
|
||||
move = Move.__table__()
|
||||
|
||||
if delta is None:
|
||||
delta = relativedelta(years=1)
|
||||
if date is None:
|
||||
date = Date.today()
|
||||
purchase_date = date - delta
|
||||
|
||||
effective_date = Coalesce(
|
||||
move.effective_date,
|
||||
Case((move.planned_date > date, move.planned_date),
|
||||
else_=date))
|
||||
|
||||
query = (purchase
|
||||
.join(purchase_line,
|
||||
condition=((purchase_line.purchase == purchase.id)
|
||||
& (purchase_line.type == 'line')))
|
||||
.join(product,
|
||||
condition=purchase_line.product == product.id)
|
||||
.join(table,
|
||||
condition=((purchase_line.product_supplier == table.id)
|
||||
| (
|
||||
(purchase_line.product_supplier == Null)
|
||||
& (purchase.party == table.party)
|
||||
& (
|
||||
(purchase_line.product == table.product)
|
||||
| ((product.template == table.template)
|
||||
& (table.product == Null))))))
|
||||
.join(move,
|
||||
condition=(
|
||||
move.origin.like('purchase.line,%')
|
||||
& (Move.origin.sql_id(move.origin, Move)
|
||||
== purchase_line.id)))
|
||||
.select(
|
||||
table.id.as_('id'),
|
||||
DateTrunc(
|
||||
'minute',
|
||||
Avg(Age(effective_date, purchase.purchase_date))
|
||||
).as_('lead_time'),
|
||||
where=(
|
||||
purchase.state.in_(['processing', 'done'])
|
||||
& (purchase.purchase_date >= purchase_date)
|
||||
& (purchase.delivery_date == Null)
|
||||
& (purchase_line.delivery_date_store == Null)
|
||||
& fields.SQL_OPERATORS['in'](
|
||||
table.id, [ps.id for ps in product_suppliers])
|
||||
),
|
||||
group_by=[table.id]))
|
||||
if backend.name == 'sqlite':
|
||||
sqlite_apply_types(query, [None, 'INTERVAL'])
|
||||
cursor.execute(*query)
|
||||
return dict(cursor)
|
||||
|
||||
def get_rec_name(self, name):
|
||||
if not self.name and not self.code:
|
||||
if self.product:
|
||||
@@ -403,15 +486,13 @@ class ProductSupplier(
|
||||
elif self.template:
|
||||
return self.template.purchase_uom
|
||||
|
||||
@property
|
||||
def lead_time_used(self):
|
||||
# Use getattr because it can be called with an unsaved instance
|
||||
lead_time = getattr(self, 'lead_time', None)
|
||||
party = getattr(self, 'party', None)
|
||||
if lead_time is None and party:
|
||||
company = getattr(self, 'company', None)
|
||||
lead_time = party.get_multivalue(
|
||||
'supplier_lead_time', company=company.id if company else None)
|
||||
@fields.depends('lead_time', 'party', 'company')
|
||||
def on_change_with_lead_time_used(self, name=None):
|
||||
lead_time = self.lead_time
|
||||
if lead_time is None and self.party:
|
||||
lead_time = self.party.get_multivalue(
|
||||
'supplier_lead_time',
|
||||
company=self.company.id if self.company else None)
|
||||
return lead_time
|
||||
|
||||
def compute_supply_date(self, date=None):
|
||||
@@ -446,6 +527,15 @@ class ProductSupplier(
|
||||
pattern['id'] = context['product_supplier']
|
||||
return pattern
|
||||
|
||||
@classmethod
|
||||
def view_attributes(cls):
|
||||
return super().view_attributes() + [
|
||||
('/tree/field[@name="average_lead_time"]',
|
||||
'visual',
|
||||
If(Eval('average_lead_time', 0) > Eval('lead_time_used', 0),
|
||||
'danger', '')),
|
||||
]
|
||||
|
||||
|
||||
class ProductSupplierPrice(
|
||||
sequence_ordered(), ModelSQL, ModelView, MatchMixin):
|
||||
|
||||
@@ -51,7 +51,10 @@ exclude = ['doc']
|
||||
"" = "trytond/modules/purchase"
|
||||
|
||||
[tool.hatch.metadata.hooks.tryton]
|
||||
dependencies = ['python-sql >= 1.4']
|
||||
dependencies = [
|
||||
'python-dateutil',
|
||||
'python-sql >= 1.4',
|
||||
]
|
||||
copyright = 'COPYRIGHT'
|
||||
readme = 'README.rst'
|
||||
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
=======================================================
|
||||
Purchase Average Lead Time of Product Supplier Scenario
|
||||
=======================================================
|
||||
|
||||
Imports::
|
||||
|
||||
>>> import datetime as dt
|
||||
>>> from decimal import Decimal
|
||||
|
||||
>>> from proteus import Model
|
||||
>>> from trytond.modules.account.tests.tools import create_chart, get_accounts
|
||||
>>> from trytond.modules.company.tests.tools import create_company
|
||||
>>> from trytond.tests.tools import activate_modules
|
||||
|
||||
>>> today = dt.date.today()
|
||||
|
||||
Activate modules::
|
||||
|
||||
>>> config = activate_modules('purchase', create_company, create_chart)
|
||||
|
||||
>>> Party = Model.get('party.party')
|
||||
>>> ProductCategory = Model.get('product.category')
|
||||
>>> ProductSupplier = Model.get('purchase.product_supplier')
|
||||
>>> ProductTemplate = Model.get('product.template')
|
||||
>>> Purchase = Model.get('purchase.purchase')
|
||||
>>> UoM = Model.get('product.uom')
|
||||
|
||||
Create suppliers::
|
||||
|
||||
>>> supplier1 = Party(name="Supplier 1")
|
||||
>>> supplier1.save()
|
||||
>>> supplier2 = Party(name="Supplier 2")
|
||||
>>> supplier2.save()
|
||||
|
||||
Create account category::
|
||||
|
||||
>>> accounts = get_accounts()
|
||||
|
||||
>>> account_category = ProductCategory(name="Account", accounting=True)
|
||||
>>> account_category.account_expense = accounts['expense']
|
||||
>>> account_category.save()
|
||||
|
||||
Create product::
|
||||
|
||||
>>> unit, = UoM.find([('name', '=', "Unit")])
|
||||
|
||||
>>> template = ProductTemplate(name="Product")
|
||||
>>> template.default_uom = unit
|
||||
>>> template.type = 'goods'
|
||||
>>> template.purchasable = True
|
||||
>>> template.account_category = account_category
|
||||
>>> _ = template.product_suppliers.new(party=supplier1)
|
||||
>>> template.save()
|
||||
>>> product, = template.products
|
||||
>>> _ = product.product_suppliers.new(party=supplier2)
|
||||
>>> product.save()
|
||||
|
||||
Create purchases::
|
||||
|
||||
>>> purchase = Purchase(party=supplier1)
|
||||
>>> purchase.purchase_date = today - dt.timedelta(days=12)
|
||||
>>> line = purchase.lines.new(product=product)
|
||||
>>> line.quantity = 1
|
||||
>>> line.unit_price = Decimal('42.0000')
|
||||
>>> purchase.click('quote')
|
||||
>>> purchase.click('confirm')
|
||||
>>> purchase.state
|
||||
'processing'
|
||||
|
||||
>>> purchase = Purchase(party=supplier1)
|
||||
>>> purchase.purchase_date = today - dt.timedelta(days=6)
|
||||
>>> line = purchase.lines.new(product=product)
|
||||
>>> line.quantity = 1
|
||||
>>> line.unit_price = Decimal('42.0000')
|
||||
>>> purchase.click('quote')
|
||||
>>> purchase.click('confirm')
|
||||
>>> purchase.state
|
||||
'processing'
|
||||
|
||||
>>> purchase = Purchase(party=supplier2)
|
||||
>>> purchase.purchase_date = today - dt.timedelta(days=5)
|
||||
>>> line = purchase.lines.new(product=product)
|
||||
>>> line.quantity = 1
|
||||
>>> line.unit_price = Decimal('42.0000')
|
||||
>>> line.product_supplier = None
|
||||
>>> purchase.click('quote')
|
||||
>>> purchase.click('confirm')
|
||||
>>> purchase.state
|
||||
'processing'
|
||||
|
||||
Check average lead time of product suppliers::
|
||||
|
||||
>>> product_supplier1, = ProductSupplier.find([('party', '=', supplier1)])
|
||||
>>> product_supplier1.average_lead_time
|
||||
datetime.timedelta(days=9)
|
||||
|
||||
>>> product_supplier2, = ProductSupplier.find([('party', '=', supplier2)])
|
||||
>>> product_supplier2.average_lead_time
|
||||
datetime.timedelta(days=5)
|
||||
@@ -8,4 +8,6 @@ this repository contains the full copyright notices and license terms. -->
|
||||
<field name="party" expand="2"/>
|
||||
<field name="name" expand="1"/>
|
||||
<field name="code" expand="1"/>
|
||||
<field name="lead_time_used" optional="1"/>
|
||||
<field name="average_lead_time" optional="1"/>
|
||||
</tree>
|
||||
|
||||
@@ -8,4 +8,6 @@ this repository contains the full copyright notices and license terms. -->
|
||||
<field name="party" expand="2"/>
|
||||
<field name="name" expand="1"/>
|
||||
<field name="code" expand="1"/>
|
||||
<field name="lead_time_used" optional="1"/>
|
||||
<field name="average_lead_time" optional="1"/>
|
||||
</tree>
|
||||
|
||||
Reference in New Issue
Block a user