Supply module: initial commit

This commit is contained in:
Bertrand Chenal
2008-09-04 23:27:49 +02:00
commit f1e1dd357d
10 changed files with 772 additions and 0 deletions
+6
View File
@@ -0,0 +1,6 @@
#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 order_point import *
from product import *
from purchase_request import *
from supply_schedule import *
+23
View File
@@ -0,0 +1,23 @@
#This file is part of Tryton. The COPYRIGHT file at the top level of this repository contains the full copyright notices and license terms.
{
'name': 'Supply Management',
'version': '0.1',
'author': 'B2CK',
'email': 'info@b2ck.com',
'website': 'http://www.tryton.org/',
'category': 'Generic',
'description': 'Supply Management Module',
'depends': [
'product',
'stock',
'purchase',
],
'xml': [
'product.xml',
'order_point.xml',
'purchase_request.xml',
'supply_schedule.xml',
],
'translation': [
]
}
+41
View File
@@ -0,0 +1,41 @@
#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 trytond.osv import fields, OSV
from trytond.wizard import Wizard, WizardOSV
class OrderPoint(OSV):
"""
Order Point: Provide a way to define a supply policy for each
product on each locations. Order points on warehouse are
conciderer by the supply scheduler to generate purchase requests.
"""
_name = 'supply.order_point'
_description = __doc__
product = fields.Many2One(
'product.product', 'Product', required=True, select=True)
location = fields.Many2One(
'stock.location', 'Location', required=True, select=True)
min_quantity = fields.Float('Minimal Quantity', required=True)
max_quantity = fields.Float('Maximal Quantity', required=True)
company = fields.Many2One('company.company', 'Company', required=True)
def default_company(self, cursor, user, context=None):
company_obj = self.pool.get('company.company')
if context is None:
context = {}
if context.get('company'):
return company_obj.name_get(cursor, user, context['company'],
context=context)[0]
return False
def __init__(self):
super(OrderPoint, self).__init__()
self._sql_constraints += [
('product_location_uniq', 'UNIQUE(product,location,company)',
'Only one order point is allowed for each product-location pair.'),
('check_min_max_quantity',
'CHECK( max_quantity is null or min_quantity is null or max_quantity >= min_quantity )',
'Maximal quantity must be bigger than Minimal quantity'),
]
OrderPoint()
+59
View File
@@ -0,0 +1,59 @@
<?xml version="1.0"?>
<!-- This file is part of Tryton. The COPYRIGHT file at the top level of this repository contains the full copyright notices and license terms. -->
<tryton>
<data>
<record model="ir.ui.view" id="order_point_view_form">
<field name="model">supply.order_point</field>
<field name="type">form</field>
<field name="arch" type="xml">
<![CDATA[
<form string="Order Point">
<label name="product"/>
<field name="product"/>
<label name="location"/>
<field name="location"/>
<label name="company"/>
<field name="company"/>
<label name="min_quantity"/>
<field name="min_quantity"/>
<label name="max_quantity"/>
<field name="max_quantity"/>
</form>
]]>
</field>
</record>
<record model="ir.ui.view" id="order_point_view_tree">
<field name="model">supply.order_point</field>
<field name="type">tree</field>
<field name="arch" type="xml">
<![CDATA[
<tree string="Locations">
<field name="product" select="1"/>
<field name="location" select="1"/>
<field name="company"/>
<field name="min_quantity" select="2"/>
<field name="max_quantity" select="2"/>
</tree>
]]>
</field>
</record>
<record model="ir.action.act_window" id="act_order_point_form">
<field name="name">Order Points</field>
<field name="res_model">supply.order_point</field>
<field name="view_type">form</field>
</record>
<record model="ir.action.act_window.view" id="act_order_point_form_view1">
<field name="sequence" eval="10"/>
<field name="view" ref="order_point_view_tree"/>
<field name="act_window" ref="act_order_point_form"/>
</record>
<record model="ir.action.act_window.view" id="act_order_point_form_view2">
<field name="sequence" eval="20"/>
<field name="view" ref="order_point_view_form"/>
<field name="act_window" ref="act_order_point_form"/>
</record>
<menuitem parent="stock.menu_stock" sequence="60"
action="act_order_point_form" id="menu_order_point_form"/>
</data>
</tryton>
+20
View File
@@ -0,0 +1,20 @@
#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 trytond.osv import fields, OSV
import datetime
class Product(OSV):
"Product"
_name = "product.product"
order_points = fields.One2Many(
'supply.order_point', 'product', 'Order Points')
Product()
class ProductSupplier(OSV):
'Product Supplier'
_name = 'purchase.product_supplier'
lead_time = fields.Integer("Lead Time") # XXX put it on the party obj ?
ProductSupplier()
+24
View File
@@ -0,0 +1,24 @@
<?xml version="1.0"?>
<!-- This file is part of Tryton. The COPYRIGHT file at the top level of this repository contains the full copyright notices and license terms. -->
<tryton>
<data>
<record model="ir.ui.view" id="product_supplier_view_form">
<field name="model">purchase.product_supplier</field>
<field name="type">form</field>
<field name="inherit" ref="purchase.product_supplier_view_form"/>
<field name="arch" type="xml">
<![CDATA[
<data>
<xpath
expr="/form//field[@name=&quot;code&quot;]" position="after">
<label name="lead_time"/>
<field name="lead_time"/>
</xpath>
</data>
]]>
</field>
</record>
</data>
</tryton>
+198
View File
@@ -0,0 +1,198 @@
#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 trytond.osv import fields, OSV
from trytond.wizard import Wizard, WizardOSV
import datetime
# TODO ensure that the link p_request p_line is never inconsistent
# (uom, qty, product, ...).
class PurchaseRequest(OSV):
'Purchase Request'
_name = 'supply.purchase_request'
_description = __doc__
product = fields.Many2One(
'product.product', 'Product', required=True, select=True)
party = fields.Many2One('relationship.party', 'Party', select=True)
quantity = fields.Float('Quantity', required=True)
uom = fields.Many2One('product.uom', 'UOM', required=True, select=True)
purchase_date = fields.Date('Expected Purchase Date')
supply_date = fields.Date('Expected Suplly Date')
stock_level = fields.Float('Stock at Supply Date')
warehouse = fields.Many2One(
'stock.location', "Warehouse", required=True,
domain="[('type', '=', 'warehouse')]")
purchase_line = fields.Many2One('purchase.line', 'Purchase Line')
company = fields.Many2One('company.company', 'Company', required=True)
def default_company(self, cursor, user, context=None):
company_obj = self.pool.get('company.company')
if context is None:
context = {}
if context.get('company'):
return company_obj.name_get(cursor, user, context['company'],
context=context)[0]
return False
PurchaseRequest()
class CreatePurchaseAsk(WizardOSV):
_name = 'supply.purchase_request.create_purchase.ask'
party = fields.Many2One('relationship.party', 'Supplier', readonly=True)
company = fields.Many2One('company.company', 'Company', readonly=True)
payment_term = fields.Many2One(
'account.invoice.payment_term', 'Payment Term', required=True)
CreatePurchaseAsk()
class CreatePurchase(Wizard):
'Create Purchase'
_name = 'supply.purchase_request.create_purchase'
states = {
'init': {
'result': {
'type': 'action',
'action': '_compute_purchase',
'state': 'choice',
},
},
'choice': {
'result': {
'type': 'choice',
'next_state': '_check_payment_term',
},
},
'ask_user': {
'actions': ['_set_default'],
'result': {
'type': 'form',
'object': 'supply.purchase_request.create_purchase.ask',
'state': [
('end', 'Cancel', 'tryton-cancel'),
('choice', 'Continue', 'tryton-ok', True),
],
},
},
'create': {
'result': {
'type': 'action',
'action': '_create_purchase',
'state': 'end',
},
},
}
def _set_default(self, cursor, user, data, context=None):
if not data.get('party_wo_pt'):
return {}
party, company = data['party_wo_pt'].pop()
return {'party': party,'company': company}
def _check_payment_term(self, cursor, user, data, context=None):
party_obj = self.pool.get('relationship.party')
if 'purchases' not in data:
return 'end'
form = data['form']
if form.get('payment_term') and form.get('party') and \
form.get('company'):
for key, val in data['purchases'].iteritems():
if (key[0], key[1]) == (form['party'],
form['company']):
val['payment_term'] = form['payment_term']
local_context = context.copy()
local_context['company'] = form['company']
party_obj.write(
cursor, user, form['party'],
{'supplier_payment_term': form['payment_term']}, context=local_context)
if data.get('party_wo_pt'):
return 'ask_user'
return 'create'
def _compute_purchase(self, cursor, user, data, context=None):
request_obj = self.pool.get('supply.purchase_request')
requests = request_obj.browse(cursor, user, data['ids'], context=context)
purchases = {}
for request in requests:
if (not request.party) or request.purchase_line:
continue
key = (request.party.id, request.company.id, request.warehouse.id)
if key not in purchases:
purchase = {
'company': request.company.id,
'party': request.party.id,
'purchase_date': request.purchase_date or datetime.date.today(),
'payment_term': request.party.supplier_payment_term and \
request.party.supplier_payment_term.id or None,
'warehouse': request.warehouse.id,
'currency': request.company.currency.id,
'lines': [],
}
purchases[key] = purchase
else:
purchase = purchases[key]
purchase['lines'].append({
'product': request.product.id,
'unit': request.uom.id,
'quantity': request.quantity,
'request': request.id,
})
if request.purchase_date:
if not purchase['purchase_date']:
purchase['purchase_date'] = request.purchase_date
else:
purchase['purchase_date'] = min(purchase['purchase_date'],
request.purchase_date)
data['purchases'] = purchases
data['party_wo_pt'] = set(
k[:2] for k in purchases if not purchases[k]['payment_term'])
return {}
def _create_purchase(self, cursor, user, data, context=None):
request_obj = self.pool.get('supply.purchase_request')
purchase_obj = self.pool.get('purchase.purchase')
line_obj = self.pool.get('purchase.line')
product_obj = self.pool.get('product.product')
created_ids = []
products = []
# collect product names
for purchase in data['purchases'].itervalues():
for line in purchase['lines']:
products.append(line['product'])
product_name = product_obj.name_get(
cursor, user, products, context=context)
# create purchases, lines and update requests
for purchase in data['purchases'].itervalues():
purchase_lines = purchase.pop('lines')
purchase_id = purchase_obj.create(
cursor, user, purchase, context=context)
created_ids.append(purchase_id)
for line in purchase_lines:
request_id = line.pop('request')
line['purchase'] = purchase_id
line['description'] = product_name[line['product']][1]
local_context = context.copy()
local_context['uom'] = line['unit']
local_context['supplier'] = purchase['party']
local_context['currency'] = purchase['currency']
product_price = product_obj.get_purchase_price(
cursor, user, [line['product']], line['quantity'],
context=local_context)[line['product']]
line['unit_price'] = product_price
line_id = line_obj.create(cursor, user, line, context=context)
request_obj.write(
cursor, user, request_id, {'purchase_line': line_id},
context=context)
return {}
CreatePurchase()
+103
View File
@@ -0,0 +1,103 @@
<?xml version="1.0"?>
<!-- This file is part of Tryton. The COPYRIGHT file at the top level of this repository contains the full copyright notices and license terms. -->
<tryton>
<data>
<record model="ir.ui.view" id="purchase_request_view_form">
<field name="model">supply.purchase_request</field>
<field name="type">form</field>
<field name="arch" type="xml">
<![CDATA[
<form string="Purchase Request">
<label name="product"/>
<field name="product"/>
<label name="party"/>
<field name="party"/>
<label name="quantity"/>
<field name="quantity"/>
<label name="uom"/>
<field name="uom"/>
<label name="warehouse"/>
<field name="warehouse"/>
<label name="purchase_date"/>
<field name="purchase_date"/>
<label name="supply_date"/>
<field name="supply_date"/>
<label name="purchase_line"/>
<field name="purchase_line"/>
<label name="stock_level"/>
<field name="stock_level"/>
<label name="company"/>
<field name="company"/>
</form>
]]>
</field>
</record>
<record model="ir.ui.view" id="purchase_request_view_tree">
<field name="model">supply.purchase_request</field>
<field name="type">tree</field>
<field name="arch" type="xml">
<![CDATA[
<tree string="Locations">
<field name="product" select="1"/>
<field name="party" select="1"/>
<field name="quantity" select="2"/>
<field name="uom" select="2"/>
<field name="warehouse" select="2"/>
<field name="purchase_date"/>
<field name="supply_date"/>
<field name="purchase_line"/>
<field name="stock_level"/>
<field name="company"/>
</tree>
]]>
</field>
</record>
<record model="ir.action.act_window" id="act_purchase_request_form">
<field name="name">Purchase Requests</field>
<field name="res_model">supply.purchase_request</field>
<field name="view_type">form</field>
</record>
<record model="ir.action.act_window.view" id="act_purchase_request_form_view1">
<field name="sequence" eval="10"/>
<field name="view" ref="purchase_request_view_tree"/>
<field name="act_window" ref="act_purchase_request_form"/>
</record>
<record model="ir.action.act_window.view" id="act_purchase_request_form_view2">
<field name="sequence" eval="20"/>
<field name="view" ref="purchase_request_view_form"/>
<field name="act_window" ref="act_purchase_request_form"/>
</record>
<menuitem parent="purchase.menu_purchase" sequence="20"
action="act_purchase_request_form" id="menu_purchase_request_form"/>
<record model="ir.ui.view" id="create_purchase.ask_form">
<field name="model">supply.purchase_request.create_purchase.ask</field>
<field name="type">form</field>
<field name="arch" type="xml">
<![CDATA[
<form string="Create Purchase Request: Missing Payment Term" col="2" cursor="payment_term">
<label name="party"/>
<field name="party"/>
<label name="company"/>
<field name="company"/>
<label name="payment_term"/>
<field name="payment_term"/>
</form>
]]>
</field>
</record>
<record model="ir.action.wizard" id="wizard_create_purchase">
<field name="name">Create Purchase</field>
<field name="wiz_name">supply.purchase_request.create_purchase</field>
</record>
<record model="ir.action.keyword" id="create_purchase_keyword">
<field name="keyword">form_action</field>
<field name="model">supply.purchase_request,0</field>
<field name="action" ref="wizard_create_purchase"/>
</record>
</data>
</tryton>
+286
View File
@@ -0,0 +1,286 @@
from trytond.wizard import Wizard, WizardOSV
import datetime
# * Generate for X day in advance ?
# * Provide a way to choose how mutch day before the shortage the suplly
# must be planned
class GeneratePurchaseRequest(Wizard):
'Generate Purchase Requests'
_name = 'supply.generate_purchase_request'
states = {
'init': {
'result': {
'type': 'action',
'action': 'generate_requests',
'state': 'end',
},
},
}
def generate_requests(self, cursor, user, data, context=None):
"""
For each order point compute the purchase that must be create
today to meet product outputs.
"""
order_point_obj = self.pool.get('supply.order_point')
purchase_request_obj = self.pool.get('supply.purchase_request')
product_obj = self.pool.get('product.product')
order_point_ids = order_point_obj.search(
cursor, user, [], context=context)
order_points = order_point_obj.browse( #XXX filter on company ??
cursor, user, order_point_ids, context=context)
product_ids = [op.product.id for op in order_points]
products = product_obj.browse(cursor, user, product_ids, context=context)
local_context = context.copy()
new_requests = []
for product in products:
# Get min and max supply dates
min_date, max_date = self.get_supply_dates(
cursor, user, product, context=context)
# Fetch all locations for whose this product has an order point
locations = [op.location.id for op in product.order_points]
# Compute stock
local_context.update({'stock_date_end': min_date or datetime.date.max})
pbl = product_obj.products_by_location(
cursor, user, locations, [product.id], with_childs=True,
skip_zero=False, context=local_context)
print "DATES",min_date, max_date, pbl
for order_point in product.order_points:
# ignore order point on other locations than warehouse
if order_point.location.type != 'warehouse':
continue
print "OP",order_point.id, order_point.location.id, product.id
# Search for shortage between min-max
shortage_date, stock_quantity = self.get_shortage(
cursor, user, order_point, min_date, max_date,
min_date_stock=pbl[order_point.location.id, product.id],
context=context)
print "REQUEST",order_point.product.name,order_point.location.name, shortage_date, stock_quantity
if shortage_date == None or stock_quantity == None:
continue
request_val = self.compute_request(
cursor, user, order_point, shortage_date,
stock_quantity, context=context)
new_requests.append(request_val)
self.create_requests(cursor, user, new_requests, context=context)
return {}
def create_requests(self, cursor, user, new_requests, context=None):
"""
Compare new_requests with already existing request and avoid
to re-create existing requests.
"""
# delete purchase request without a purchase line
uom_obj = self.pool.get('product.uom')
request_obj = self.pool.get('supply.purchase_request')
product_supplier_obj = self.pool.get('purchase.product_supplier')
req_ids = request_obj.search(
cursor, user, [('purchase_line', '=', False)], context=context)
request_obj.delete(cursor, user, req_ids, context=context)
req_ids = request_obj.search(cursor, user, [], context=context)
requests = request_obj.browse(cursor, user, req_ids, context=context)
# Fetch lead_times for each (product,supplier)
sup_lead_time = {}
for request in requests:
product, supplier = None, None
if request.purchase_line:
product = request.purchase_line.product.id
supplier = request.purchase_line.purchase.party.id
else:
product = request.product.id
supplier = request.party and request.party.id
if not supplier:
continue
sup_lead_time[product, supplier] = None
print "TUPLE",['OR', ] + [
['AND', ('product', '=', x[0]), ('party', '=', x[1])] \
for x in sup_lead_time.iterkeys()
]
prod_sup_ids = product_supplier_obj.search(
cursor, user,
['OR', ] + [
['AND', ('product', '=', x[0]), ('party', '=', x[1])] \
for x in sup_lead_time.iterkeys()
],
context=context)
for prod_sup in product_supplier_obj.browse(cursor, user, prod_sup_ids,
context=context):
sup_lead_time[prod_sup.product.id, prod_sup.party.id] = \
prod_sup.lead_time
# Fetch data from existing requests
existing_req = {}
for request in requests:
if request.purchase_line:
product = request.purchase_line.product
warehouse = request.purchase_line.purchase.warehouse
purchase_date = request.purchase_line.purchase.purchase_date
qty = line.quantity
uom = line.unit
supplier = line.purchase.party
else:
product = request.product
warehouse = request.warehouse
purchase_date = request.purchase_date
qty = request.quantity
uom = request.uom
supplier = request.party.id
lead_time = sup_lead_time.get((product.id, supplier.id))
if lead_time:
supply_date = purchase_date + \
datetime.timedelta(lead_time)
else:
supply_date = datetime.date.max
existing_req.setdefault((product.id, warehouse.id), []).append(
{'supply_date': supply_date, 'quantity': qty, 'uom': uom})
for i in existing_req.itervalues():
i.sort
print "OLD_REQ",existing_req
print "NEW_REQ", new_requests
# Update new requests to take existing requests into account
new_requests.sort(lambda r,s: cmp(r['supply_date'],s['supply_date']))
for new_req in new_requests:
for old_req in existing_req.get((new_req['product'].id,
new_req['warehouse'].id), []):
print old_req['supply_date'] <= new_req['supply_date'], old_req['supply_date'], new_req['supply_date']
if old_req['supply_date'] <= new_req['supply_date']:
quantity = uom_obj.compute_qty(
cursor, user, old_req['uom'], old_req['quantity'],
new_req['uom'], context=context)
new_req['quantity'] = max(0.0, new_req['quantity'] - quantity)
old_req['quantity'] = uom_obj.compute_qty(
cursor, user, new_req['uom'],
max(0.0, quantity - new_req['quantity']),
old_req['uom'], context=context)
else:
break
for new_req in new_requests:
if new_req['quantity'] > 0.0:
new_req.update({'product': new_req['product'].id,
'party': new_req['party'] and new_req['party'].id,
'uom': new_req['uom'].id,
'company': new_req['company'].id
})
print "CREATE", new_req
request_obj.create(cursor, user, new_req, context=context)
def get_supply_dates(self, cursor, user, product, context=None):
"""
Return for the given product min and max values for the
earliest supply dates across all available suppliers.
"""
min_date = None
max_date = None
today = datetime.date.today()
for product_supplier in product.product_suppliers:
supply_date = today + datetime.timedelta(product_supplier.lead_time)
if (not min_date) or supply_date < min_date:
min_date = supply_date
if (not max_date) or supply_date > max_date:
max_date = supply_date
return (min_date, max_date)
def compute_request(self, cursor, user, order_point, shortage_date,
stock_quantity, context=None):
"""
Return the value of the purchase request which will answer to
the needed quantity at the given date. I.e: the latest
purchase date, the expected supply date and the prefered
supplier.
"""
product = order_point.product
supplier = None
seq = None
on_time = False
today = datetime.date.today()
for product_supplier in product.product_suppliers:
supply_date = today + datetime.timedelta(product_supplier.lead_time)
sup_on_time = supply_date < shortage_date
if not supplier:
supplier = product_supplier.party
on_time = sup_on_time
seq = product_supplier.sequence
continue
if (sup_on_time, product_supplier.sequence) < (on_time, seq):
supplier = product_supplier.party
on_time = sup_on_time
seq = product_supplier.sequence
if supplier and product_supplier.lead_time:
purchase_date = \
shortage_date - datetime.timedelta(product_supplier.lead_time)
else:
purchase_date = today
return {'product': product,
'party': supplier and supplier or None,
'quantity': order_point.max_quantity - stock_quantity,
'uom': product.default_uom,
'purchase_date': purchase_date,
'supply_date': shortage_date,
'stock_level': stock_quantity,
'company': order_point.company,
'warehouse': order_point.location,
}
def get_shortage(self, cursor, user, order_point, min_date,
max_date, min_date_stock, context=None):
"""
Compute stock quantities between the two given dates. If given
the order point, one date will be lacking in products this
date is returned alongside the stock level at this date.
If no dates are given, the stock is computed for an infinire
date and the shortage date is today.
"""
product_obj = self.pool.get('product.product')
if not min_date:
if min_date_stock < order_point.min_quantity:
return (
datetime.date.today(), min_date_stock)
else:
return (None, None)
if not max_date: max_date = min_date
current_date = min_date
current_stock = min_date_stock
while current_date <= max_date:
print "SHTG", current_stock, order_point.min_quantity
if current_stock < order_point.min_quantity:
return current_date, current_stock
local_context = context.copy()
local_context['stock_date_start'] = current_date
local_context['stock_date_end'] = current_date
res = product_obj.products_by_location(
cursor, user, [order_point.location.id],
[order_point.location.id], with_childs=True, skip_zero=False,
context=context)
for qty in res.itervalues():
current_stock += qty
current_date += datetime.timedelta(1)
return (None, None)
GeneratePurchaseRequest()
+12
View File
@@ -0,0 +1,12 @@
<?xml version="1.0"?>
<!-- This file is part of Tryton. The COPYRIGHT file at the top level of this repository contains the full copyright notices and license terms. -->
<tryton>
<data>
<record model="ir.action.wizard" id="act_generate_purchase_request">
<field name="name">Generate Purchase Requests</field>
<field name="wiz_name">supply.generate_purchase_request</field>
</record>
<menuitem parent="purchase.menu_purchase" action="act_generate_purchase_request"
id="menu_generate_purchase_request"/>
</data>
</tryton>