Rename module from production_external to production_subcontract.

Make basic workflow work. Missing parts:

- Synchronize quantities between production and internal shipment
- Improve internal shipment or change shipment type so multiple locations can
  be used in the same document.
- Allow using produced quantities as supplier invoice quantity
This commit is contained in:
Albert Cervera i Areny 2016-03-07 09:18:46 +01:00
parent c4952629de
commit 26e104890c
13 changed files with 452 additions and 86 deletions

2
README
View File

@ -20,7 +20,7 @@ questions on the NaN·tic bug tracker, mailing list,
wiki or IRC channel: wiki or IRC channel:
* http://doc.tryton-erp.es/ * http://doc.tryton-erp.es/
* http://bitbucket.org/nantic/trytond-production_external * http://bitbucket.org/nantic/trytond-production_subcontract
* http://groups.tryton.org/ * http://groups.tryton.org/
* http://wiki.tryton.org/ * http://wiki.tryton.org/
* irc://irc.freenode.net/tryton * irc://irc.freenode.net/tryton

View File

@ -7,6 +7,7 @@ def register():
Pool.register( Pool.register(
Party, Party,
PurchaseRequest, PurchaseRequest,
BOM,
Production, Production,
Purchase, Purchase,
module='production_external', type_='model') module='production_subcontract', type_='model')

View File

@ -1,29 +1,29 @@
# The COPYRIGHT file at the top level of this repository contains the full # The COPYRIGHT file at the top level of this repository contains the full
# copyright notices and license terms. # copyright notices and license terms.
from trytond.pool import Pool, PoolMeta from trytond.pool import Pool, PoolMeta
from trytond.model import ModelView, fields from trytond.model import ModelView, Workflow, fields
from trytond.pyson import Eval from trytond.pyson import Eval, Bool
__all__ = ['Party', 'PurchaseRequest', 'Production', 'Purchase'] __all__ = ['Party', 'PurchaseRequest', 'BOM', 'Production', 'Purchase']
__metaclass__ = PoolMeta
class Party: class Party:
__name__ = 'party.party' __name__ = 'party.party'
# Should be a property __metaclass__ = PoolMeta
# Should probably be external_warehouse # TODO: Should be a property
external_location = fields.Many2One('stock.location', 'External Location', production_warehouse = fields.Property(fields.Many2One('stock.location',
domain=[ 'Production Warehouse', domain=[
('type', '=', 'storage'), ('type', '=', 'warehouse'),
]) ]))
class PurchaseRequest: class PurchaseRequest:
__name__ = 'purchase.request' __name__ = 'purchase.request'
__metaclass__ = PoolMeta
@classmethod @classmethod
def origin_get(cls): def get_origin(cls):
res = super(PurchaseRequest, cls).origin_get()
Model = Pool().get('ir.model') Model = Pool().get('ir.model')
res = super(PurchaseRequest, cls).get_origin()
models = Model.search([ models = Model.search([
('model', '=', 'production'), ('model', '=', 'production'),
]) ])
@ -32,17 +32,35 @@ class PurchaseRequest:
return res return res
class BOM:
__name__ = 'production.bom'
__metaclass__ = PoolMeta
subcontract_product = fields.Many2One('product.product',
'Subcontract Product', domain=[
('purchasable', '=', True),
('type', '=', 'service'),
])
class Production: class Production:
__name__ = 'production' __name__ = 'production'
__metaclass__ = PoolMeta
subcontract_product = fields.Many2One('product.product', subcontract_product = fields.Many2One('product.product',
'Subcontract Product', domain=[('purchasable', '=', True)]) 'Subcontract Product', domain=[
('purchasable', '=', True),
('type', '=', 'service'),
])
purchase_request = fields.Many2One('purchase.request', purchase_request = fields.Many2One('purchase.request',
'Purchase Request', readonly=True) 'Purchase Request', readonly=True)
outgoing_shipment = fields.Many2One('stock.shipment.internal',
'Outgoing Shipment', readonly=True)
incoming_shipment = fields.Many2One('stock.shipment.internal', incoming_shipment = fields.Many2One('stock.shipment.internal',
'Internal Shipment', readonly=True) 'Incoming Shipment', readonly=True)
destination_warehouse = fields.Many2One('stock.location',
'Destination Warehouse', domain=[
('type', '=', 'warehouse'),
], readonly=True)
supplier = fields.Function(fields.Many2One('party.party', 'Supplier'),
'get_supplier', searcher='search_supplier')
@classmethod @classmethod
def __setup__(cls): def __setup__(cls):
@ -51,114 +69,156 @@ class Production:
# created but purchase order is not in processing state. # created but purchase order is not in processing state.
cls._buttons.update({ cls._buttons.update({
'create_purchase_request': { 'create_purchase_request': {
'invisible': ~Eval('state').in_(['draft', 'waiting']), 'invisible': ~(Eval('state').in_(['draft', 'waiting']) &
Bool(Eval('subcontract_product')) &
~Bool(Eval('purchase_request'))),
'icon': 'tryton-go-home', 'icon': 'tryton-go-home',
} }
}) })
def get_supplier(self, name):
return (self.purchase_request.party.id if self.purchase_request and
self.purchase_request.party else None)
@classmethod
def search_supplier(cls, name, clause):
return [('purchase_request.party',) + tuple(clause[1:])]
@classmethod @classmethod
def copy(cls, productions, default=None): def copy(cls, productions, default=None):
if default is None: if default is None:
default = {} default = {}
default['purchase_request'] = None default['purchase_request'] = None
default['outgoing_shipment'] = None
default['incoming_shipment'] = None default['incoming_shipment'] = None
return super(Production, cls).copy(productions, default) return super(Production, cls).copy(productions, default)
@classmethod @classmethod
@ModelView.button @ModelView.button
def create_purchase_request(cls, productions): def create_purchase_request(cls, productions):
PurchaseRequest = Pool().get('purchase.request')
for production in productions: for production in productions:
if not production.subcontract_product: if not production.subcontract_product:
continue continue
if not production.state in ('draft', 'waiting'): if not production.state in ('draft', 'waiting'):
continue continue
request, = PurchaseRequest.create([{ request = production._get_purchase_request()
'product': production.subcontract_product.id, request.save()
'company': production.company.id,
'uom': production.subcontract_product.default_uom.id,
'quantity': production.quantity,
'computed_quantity': production.quantity,
'warehouse': production.warehouse.id,
'origin': ('production', production.id),
}])
production.purchase_request = request production.purchase_request = request
production.save() production.save()
def on_change_product(self):
res = super(Production, self).on_change_product()
if self.bom:
res['subcontract_product'] = (self.bom.subcontract_product.id if
self.bom.subcontract_product else None)
return res
def on_change_bom(self):
res = super(Production, self).on_change_bom()
if self.bom:
res['subcontract_product'] = (self.bom.subcontract_product.id if
self.bom.subcontract_product else None)
return res
def _get_purchase_request(self):
PurchaseRequest = Pool().get('purchase.request')
return PurchaseRequest(
product=self.subcontract_product,
company=self.company,
uom=self.subcontract_product.default_uom,
quantity=self.quantity,
computed_quantity=self.quantity,
warehouse=self.warehouse,
origin=self,
)
@classmethod @classmethod
def process_purchase_request(cls, productions): def process_purchase_request(cls, productions):
pool = Pool() pool = Pool()
ShipmentInternal = pool.get('stock.shipment.internal') ShipmentInternal = pool.get('stock.shipment.internal')
Move = pool.get('stock.move')
for production in productions: for production in productions:
# Create outgoing internal shipment if not (production.purchase_request and
shipment = ShipmentInternal() production.purchase_request.purchase and
production.purchase_request.purchase.state == 'processing'):
continue
if production.destination_warehouse:
continue
subcontract_warehouse = production._get_subcontract_warehouse()
production.destination_warehouse = production.warehouse
production.warehouse = subcontract_warehouse
from_location = production.warehouse.storage_location from_location = production.warehouse.storage_location
purchase = production.purchase_request.purchase_line.purchase to_location = production.destination_warehouse.storage_location
to_location = purchase.party.external_location
shipment.from_location = from_location
shipment.to_location = to_location
shipment.moves = []
for input_ in production.inputs:
move = Move()
move.shipment = shipment
move.from_location = from_location
move.to_location = to_location
move.product = input_.product
# TODO: Support lots
move.quantity = input_.quantity
move.uom = input_.uom
shipment.moves.append(move)
shipment.save()
production.outgoing_shipment = shipment
# Create incoming internal shipment
# TODO: Production location should be taken from the destination
# warehouse
tmp = from_location
from_location = to_location
to_location = tmp
shipment = ShipmentInternal() shipment = ShipmentInternal()
shipment.from_location = from_location shipment.from_location = from_location
shipment.to_location = to_location shipment.to_location = to_location
shipment.moves = [] shipment.moves = []
for output in production.outputs: for output in production.outputs:
move = Move() move = production._get_incoming_shipment_move(output,
move.from_location = from_location from_location, to_location)
move.to_location = to_location
move.product = output.product
# TODO: Support lots
move.quantity = output.quantity
move.uom = output.uom
shipment.moves.append(move) shipment.moves.append(move)
shipment.save() shipment.save()
ShipmentInternal.wait([shipment])
production.incoming_shipment = shipment production.incoming_shipment = shipment
location = from_location storage_location = production.warehouse.storage_location
# Update production production_location = production.warehouse.production_location
#production.warehouse =
for move in production.inputs: for move in production.inputs:
move.from_location = location move.from_location = storage_location
move.to_location = production_location
move.save() move.save()
for move in production.outputs: for move in production.outputs:
move.to_location = location move.from_location = production_location
move.to_location = storage_location
move.save() move.save()
production.save() production.save()
def _get_incoming_shipment_move(self, output, from_location, to_location):
Move = Pool().get('stock.move')
return Move(
from_location=from_location,
to_location=to_location,
product=output.product,
# TODO: Support lots
quantity=output.quantity,
uom=output.uom,
)
# Missing function to synchronize output production moves with incoming def _get_subcontract_warehouse(self):
# internal shipment. Should emulate behaviour of ShipmentOut and ShipmentIn return self.purchase_request.party.production_warehouse
# where there is no direct linke between stock moves but are calculated by
# product and quantities. See _sync_inventory_to_outgoing in @classmethod
def write(cls, *args):
actions = iter(args)
to_update = []
for productions, values in zip(actions, actions):
if 'outputs' in values:
to_update.extend(productions)
super(Production, cls).write(*args)
if to_update:
Production._sync_outputs_to_shipment(to_update)
# TODO: Missing function to synchronize output production moves with
# incoming internal shipment. Should emulate behaviour of ShipmentOut and
# ShipmentIn where there is no direct linke between stock moves but are
# calculated by product and quantities. See _sync_inventory_to_outgoing in
# stock/shipment.py. # stock/shipment.py.
@classmethod
@ModelView.button
@Workflow.transition('done')
def done(cls, productions):
InternalShipment = Pool().get('stock.shipment.internal')
super(Production, cls).done(productions)
shipments = [x.incoming_shipment for x in productions if
x.incoming_shipment]
if shipments:
InternalShipment.assign_try(shipments)
# TODO: Internal shipment should be updated each time outputs are changed
class Purchase: class Purchase:
__name__ = 'purchase.purchase' __name__ = 'purchase.purchase'
__metaclass__ = PoolMeta
@classmethod @classmethod
def process(cls, purchases): def process(cls, purchases):

View File

@ -19,5 +19,17 @@
<field name="inherit" ref="production.production_view_list"/> <field name="inherit" ref="production.production_view_list"/>
<field name="name">production_list</field> <field name="name">production_list</field>
</record> </record>
<record model="ir.ui.view" id="bom_view_form">
<field name="model">production.bom</field>
<field name="type">form</field>
<field name="inherit" ref="production.bom_view_form"/>
<field name="name">bom_form</field>
</record>
<record model="ir.ui.view" id="bom_view_list">
<field name="model">production.bom</field>
<field name="type">tree</field>
<field name="inherit" ref="production.bom_view_list"/>
<field name="name">bom_list</field>
</record>
</data> </data>
</tryton> </tryton>

View File

@ -6,7 +6,7 @@ import re
import os import os
import ConfigParser import ConfigParser
MODULE = 'production_external' MODULE = 'production_subcontract'
PREFIX = 'nantic' PREFIX = 'nantic'
MODULE2PREFIX = {} MODULE2PREFIX = {}

View File

@ -1,3 +1,3 @@
# The COPYRIGHT file at the top level of this repository contains the full # The COPYRIGHT file at the top level of this repository contains the full
# copyright notices and license terms. # copyright notices and license terms.
from .test_production_external import suite from .test_production_subcontract import suite

View File

@ -0,0 +1,267 @@
===================
Production Scenario
===================
=============
General Setup
=============
Imports::
>>> import datetime
>>> from dateutil.relativedelta import relativedelta
>>> from decimal import Decimal
>>> from proteus import config, Model, Wizard
>>> today = datetime.date.today()
>>> yesterday = today - relativedelta(days=1)
Create database::
>>> config = config.set_trytond()
>>> config.pool.test = True
Install production Module::
>>> Module = Model.get('ir.module.module')
>>> modules = Module.find([('name', '=', 'production_subcontract')])
>>> Module.install([x.id for x in modules], config.context)
>>> Wizard('ir.module.module.install_upgrade').execute('upgrade')
Create company::
>>> Currency = Model.get('currency.currency')
>>> CurrencyRate = Model.get('currency.currency.rate')
>>> Company = Model.get('company.company')
>>> Party = Model.get('party.party')
>>> company_config = Wizard('company.company.config')
>>> company_config.execute('company')
>>> company = company_config.form
>>> party = Party(name='Dunder Mifflin')
>>> party.save()
>>> company.party = party
>>> currencies = Currency.find([('code', '=', 'USD')])
>>> if not currencies:
... currency = Currency(name='Euro', symbol=u'$', code='USD',
... rounding=Decimal('0.01'), mon_grouping='[3, 3, 0]',
... mon_decimal_point=',')
... currency.save()
... CurrencyRate(date=today + relativedelta(month=1, day=1),
... rate=Decimal('1.0'), currency=currency).save()
... else:
... currency, = currencies
>>> company.currency = currency
>>> company_config.execute('add')
>>> company, = Company.find()
Reload the context::
>>> User = Model.get('res.user')
>>> config._context = User.get_preferences(True, config.context)
Create supplier warehouse::
>>> Location = Model.get('stock.location')
>>> supplier_storage = Location(name='Supplier Storage', type='storage')
>>> supplier_storage.save()
>>> supplier_input = Location(name='Supplier Input', type='storage')
>>> supplier_input.save()
>>> supplier_output = Location(name='Supplier Output', type='storage')
>>> supplier_output.save()
>>> supplier_production = Location(name='Supplier Production',
... type='storage')
>>> supplier_production.save()
>>> supplier_warehouse = Location()
>>> supplier_warehouse.type = 'warhouse'
>>> supplier_warehouse.name = 'Supplier Warehouse'
>>> supplier_warehouse.storage_location = supplier_storage
>>> supplier_warehouse.input_location = supplier_input
>>> supplier_warehouse.output_location = supplier_output
>>> supplier_warehouse.production_location = supplier_production
>>> supplier_warehouse.save()
Create supplier::
>>> Party = Model.get('party.party')
>>> party = Party(name='Supplier')
>>> party.production_warehouse = supplier_warehouse
Create product::
>>> ProductUom = Model.get('product.uom')
>>> unit, = ProductUom.find([('name', '=', 'Unit')])
>>> ProductTemplate = Model.get('product.template')
>>> Product = Model.get('product.product')
>>> product = Product()
>>> template = ProductTemplate()
>>> template.name = 'product'
>>> template.default_uom = unit
>>> template.type = 'goods'
>>> template.list_price = Decimal(30)
>>> template.cost_price = Decimal(20)
>>> template.save()
>>> product.template = template
>>> product.save()
Create Components::
>>> component1 = Product()
>>> template1 = ProductTemplate()
>>> template1.name = 'component 1'
>>> template1.default_uom = unit
>>> template1.type = 'goods'
>>> template1.list_price = Decimal(5)
>>> template1.cost_price = Decimal(1)
>>> template1.save()
>>> component1.template = template1
>>> component1.save()
>>> meter, = ProductUom.find([('name', '=', 'Meter')])
>>> centimeter, = ProductUom.find([('name', '=', 'centimeter')])
>>> component2 = Product()
>>> template2 = ProductTemplate()
>>> template2.name = 'component 2'
>>> template2.default_uom = meter
>>> template2.type = 'goods'
>>> template2.list_price = Decimal(7)
>>> template2.cost_price = Decimal(5)
>>> template2.save()
>>> component2.template = template2
>>> component2.save()
Create Subcontract Product::
>>> subcontract = Product()
>>> stemplate = ProductTemplate()
>>> stemplate.name = 'Subcontract'
>>> stemplate.default_uom = unit
>>> stemplate.type = 'service'
>>> stemplate.list_price = Decimal(0)
>>> stemplate.cost_price = Decimal(100)
>>> stemplate.save()
>>> subcontract.template = stemplate
>>> subcontract.save()
Create Bill of Material::
>>> BOM = Model.get('production.bom')
>>> BOMInput = Model.get('production.bom.input')
>>> BOMOutput = Model.get('production.bom.output')
>>> bom = BOM(name='product', subcontract_product=subcontract)
>>> input1 = BOMInput()
>>> bom.inputs.append(input1)
>>> input1.product = component1
>>> input1.quantity = 5
>>> input2 = BOMInput()
>>> bom.inputs.append(input2)
>>> input2.product = component2
>>> input2.quantity = 150
>>> input2.uom = centimeter
>>> output = BOMOutput()
>>> bom.outputs.append(output)
>>> output.product = product
>>> output.quantity = 1
>>> bom.save()
>>> ProductBom = Model.get('product.product-production.bom')
>>> product.boms.append(ProductBom(bom=bom))
>>> product.save()
Create an Inventory::
>>> Inventory = Model.get('stock.inventory')
>>> InventoryLine = Model.get('stock.inventory.line')
>>> Location = Model.get('stock.location')
>>> storage = supplier_warehouse.storage_location
>>> inventory = Inventory()
>>> inventory.location = storage
>>> inventory_line1 = InventoryLine()
>>> inventory.lines.append(inventory_line1)
>>> inventory_line1.product = component1
>>> inventory_line1.quantity = 20
>>> inventory_line2 = InventoryLine()
>>> inventory.lines.append(inventory_line2)
>>> inventory_line2.product = component2
>>> inventory_line2.quantity = 6
>>> inventory.save()
>>> Inventory.confirm([inventory.id], config.context)
>>> inventory.state
u'done'
Make a production::
>>> warehouse = Location.find(['code', '=', 'WH'])
>>> Production = Model.get('production')
>>> production = Production()
>>> production.warehouse = warehouse
>>> production.product = product
>>> production.bom = bom
>>> production.quantity = 2
>>> sorted([i.quantity for i in production.inputs]) == [10, 300]
True
>>> output, = production.outputs
>>> output.quantity == 2
True
>>> production.cost
Decimal('25.0')
>>> production.save()
>>> Production.wait([production.id], config.context)
>>> production.state
u'waiting'
>>> Production.assign_try([production.id], config.context)
True
>>> production.reload()
>>> all(i.state == 'assigned' for i in production.inputs)
True
>>> Production.run([production.id], config.context)
>>> production.reload()
>>> all(i.state == 'done' for i in production.inputs)
True
>>> len(set(i.effective_date == today for i in production.inputs))
1
>>> Production.done([production.id], config.context)
>>> production.reload()
>>> output, = production.outputs
>>> output.state
u'done'
>>> output.effective_date == production.effective_date
True
>>> config._context['locations'] = [storage.id]
>>> product.reload()
>>> product.quantity == 2
True
Make a production with effective date yesterday::
>>> Production = Model.get('production')
>>> production = Production()
>>> production.effective_date = yesterday
>>> production.product = product
>>> production.bom = bom
>>> production.quantity = 2
>>> production.subcontract_product == subcontract
>>> production.click('wait')
>>> production.click('create_purchase_request')
Process purchase request::
>>> purchase_request = production.purchase_request
>>> create_purchase = Wizard('purchase.request.create_purchase',
... [purchase_request])
>>> purchase_request.reload()
>>> purchase = purchase_request.purchase
>>> purchase.click('quotation')
>>> purchase.click('confirm')
>>> purchase.click('process')
>>> production.reload()
>>> production.warehouse = supplier_warehouse
>>> production.destination_warehouse = warehouse
>>> shipment = production.incoming_shipment
Process production::
>>> Production.assign_try([production.id], config.context)
True
>>> production.click('run')
>>> production.reload()
>>> shipment.reload()
>>> shipment.state = 'reserved'

View File

@ -2,19 +2,21 @@
# The COPYRIGHT file at the top level of this repository contains the full # The COPYRIGHT file at the top level of this repository contains the full
# copyright notices and license terms. # copyright notices and license terms.
import unittest import unittest
import doctest
import trytond.tests.test_tryton import trytond.tests.test_tryton
from trytond.tests.test_tryton import test_view, test_depends from trytond.tests.test_tryton import test_view, test_depends
from trytond.tests.test_tryton import doctest_setup, doctest_teardown
class TestCase(unittest.TestCase): class TestCase(unittest.TestCase):
'Test module' 'Test module'
def setUp(self): def setUp(self):
trytond.tests.test_tryton.install_module('production_external') trytond.tests.test_tryton.install_module('production_subcontract')
def test0005views(self): def test0005views(self):
'Test views' 'Test views'
test_view('production_external') test_view('production_subcontract')
def test0006depends(self): def test0006depends(self):
'Test depends' 'Test depends'
@ -24,4 +26,7 @@ class TestCase(unittest.TestCase):
def suite(): def suite():
suite = trytond.tests.test_tryton.suite() suite = trytond.tests.test_tryton.suite()
suite.addTests(unittest.TestLoader().loadTestsFromTestCase(TestCase)) suite.addTests(unittest.TestLoader().loadTestsFromTestCase(TestCase))
suite.addTests(doctest.DocFileSuite('scenario_production.rst',
setUp=doctest_setup, tearDown=doctest_teardown, encoding='utf-8',
optionflags=doctest.REPORT_ONLY_FIRST_FAILURE))
return suite return suite

10
view/bom_form.xml Normal file
View File

@ -0,0 +1,10 @@
<?xml version="1.0"?>
<!-- The COPYRIGHT file at the top level of this repository contains the full
copyright notices and license terms. -->
<data>
<xpath expr="/form/field[@name='active']" position="after">
<label name="subcontract_product"/>
<field name="subcontract_product"/>
</xpath>
</data>

9
view/bom_list.xml Normal file
View File

@ -0,0 +1,9 @@
<?xml version="1.0"?>
<!-- The COPYRIGHT file at the top level of this repository contains the full
copyright notices and license terms. -->
<data>
<xpath expr="/tree/field[@name='name']" position="after">
<field name="subcontract_product"/>
</xpath>
</data>

View File

@ -3,7 +3,7 @@
copyright notices and license terms. --> copyright notices and license terms. -->
<data> <data>
<xpath expr="/form/notebook/page[@id='stock']" position="inside"> <xpath expr="/form/notebook/page[@id='stock']" position="inside">
<label name="external_location"/> <label name="production_warehouse"/>
<field name="external_location"/> <field name="production_warehouse"/>
</xpath> </xpath>
</data> </data>

View File

@ -9,12 +9,14 @@
colspan="2"/> colspan="2"/>
</xpath> </xpath>
<xpath expr="/form/notebook/page[@id='other']/field[@name='location']" <xpath expr="/form/notebook/page[@id='other']/field[@name='location']"
position="after"> position="after">
<label name="purchase_request"/> <label name="purchase_request"/>
<field name="purchase_request"/> <field name="purchase_request"/>
<label name="outgoing_shipment"/> <label name="supplier"/>
<field name="outgoing_shipment"/> <field name="supplier"/>
<label name="incoming_shipment"/> <label name="incoming_shipment"/>
<field name="incoming_shipment"/> <field name="incoming_shipment"/>
<label name="destination_warehouse"/>
<field name="destination_warehouse"/>
</xpath> </xpath>
</data> </data>

View File

@ -3,6 +3,6 @@
copyright notices and license terms. --> copyright notices and license terms. -->
<data> <data>
<xpath expr="/tree/field[@name='reference']" position="after"> <xpath expr="/tree/field[@name='reference']" position="after">
<field name="subcontract_product"/> <field name="supplier"/>
</xpath> </xpath>
</data> </data>