Add replace wizard.

This commit refs #4825
This commit is contained in:
Sergio Morillo 2018-07-31 12:04:37 +02:00
parent cfba67818b
commit 14d96a5505
8 changed files with 392 additions and 4 deletions

View file

@ -7,4 +7,8 @@ from . import stock
def register():
Pool.register(
stock.Lot,
stock.LotReplaceAsk,
module='stock_lot_unique', type_='model')
Pool.register(
stock.LotReplace,
module='stock_lot_unique', type_='wizard')

View file

@ -2,6 +2,64 @@
msgid ""
msgstr "Content-Type: text/plain; charset=utf-8\n"
msgctxt "error:stock.lot.replace:"
msgid ""
"Cannot replace lots with Period cache. Please go back period \"%s\" to Draft"
" first."
msgstr "No puede reemplazar lotes usados en Períodos. Ponga primero estado Borrador en período \"%s\"."
msgctxt "error:stock.lot.replace:"
msgid "Lots have different numbers: %(source_name)s vs %(destination_name)s."
msgstr "Los lotes tienen números diferentes: %(source_name)s vs %(destination_name)s."
msgctxt "error:stock.lot.replace:"
msgid "Lots have different product: %(source_code)s vs %(destination_code)s."
msgstr "Los lotes tienen productos diferentes: %(source_code)s vs %(destination_code)s."
msgctxt "error:stock.lot:"
msgid "Lot number must be unique by product."
msgstr "Número de lote debe ser único por Producto."
msgstr "Número de lote debe ser único por Producto."
msgctxt "field:stock.lot.replace.ask,destination:"
msgid "Destination"
msgstr "Destino"
msgctxt "field:stock.lot.replace.ask,id:"
msgid "ID"
msgstr "Identificador"
msgctxt "field:stock.lot.replace.ask,sources:"
msgid "Sources"
msgstr "Origenes"
msgctxt "help:stock.lot.replace.ask,destination:"
msgid "The lot that replaces."
msgstr "El lote que reemplaza."
msgctxt "help:stock.lot.replace.ask,sources:"
msgid "The lots to be replaced."
msgstr "Los lotes a reemplazar."
msgctxt "model:ir.action,name:wizard_replace"
msgid "Replace"
msgstr "Reemplazar"
msgctxt "model:stock.lot.replace.ask,name:"
msgid "Replace Lot Ask"
msgstr "Pregunta reemplazar lote"
msgctxt "wizard_button:stock.lot.replace,ask,end:"
msgid "Cancel"
msgstr "Cancelar"
msgctxt "wizard_button:stock.lot.replace,ask,replace:"
msgid "Replace"
msgstr "Reemplazar"
msgctxt "view:stock.lot.replace.ask:"
msgid "Lots"
msgstr "Lotes"
msgctxt "view:stock.lot.replace.ask:"
msgid "Replace By"
msgstr "Reemplazar por"

145
stock.py
View file

@ -1,9 +1,13 @@
# The COPYRIGHT file at the top level of this repository contains the full
# copyright notices and license terms.
from trytond.model import Unique
from trytond.pool import PoolMeta
from sql import Column
from trytond.model import ModelView, fields, Unique
from trytond.pool import PoolMeta, Pool
from trytond.pyson import Eval
from trytond.transaction import Transaction
from trytond.wizard import Wizard, StateView, StateTransition, Button
__all__ = ['Lot']
__all__ = ['Lot', 'LotReplace', 'LotReplaceAsk']
class Lot:
@ -18,3 +22,138 @@ class Lot:
('lot_uniq', Unique(t, t.number, t.product),
'Lot number must be unique by product.'),
]
class LotReplace(Wizard):
"""Replace Lot"""
__name__ = 'stock.lot.replace'
start_state = 'ask'
ask = StateView('stock.lot.replace.ask',
'stock_lot_unique.replace_ask_view_form', [
Button('Cancel', 'end', 'tryton-cancel'),
Button('Replace', 'replace', 'tryton-find-replace', default=True),
])
replace = StateTransition()
@classmethod
def __setup__(cls):
super(LotReplace, cls).__setup__()
cls._error_messages.update({
'different_name':
'Lots have different numbers: '
'%(source_name)s vs %(destination_name)s.',
'different_product':
'Lots have different product: '
'%(source_code)s vs %(destination_code)s.',
'period_cache': 'Cannot replace lots with Period cache. '
'Please go back period "%s" to Draft first.'
})
def check_similarity(self):
sources = self.ask.sources
destination = self.ask.destination
for source in sources:
if source.number != destination.number:
key = 'stock.lot.replace number %s %s' % (
source.number, destination.number)
self.raise_user_warning(key, 'different_name', {
'source_name': source.number,
'destination_name': destination.number,
})
if source.product.id != destination.product.id:
self.raise_user_error('different_product', {
'source_code': source.product.rec_name,
'destination_code': destination.product.rec_name,
})
def check_period_cache(self):
pool = Pool()
Cache = pool.get('stock.period.cache.lot')
lot_ids = [s.id for s in self.ask.sources] + [self.ask.destination.id]
caches = Cache.search([('lot', 'in', lot_ids)], limit=1)
if caches:
self.raise_user_error('period_cache', caches[0].period.rec_name)
def transition_replace(self):
pool = Pool()
Lot = pool.get('stock.lot')
lot = Lot.__table__()
transaction = Transaction()
self.check_period_cache()
self.check_similarity()
source_ids = [s.id for s in self.ask.sources]
destination = self.ask.destination
cursor = transaction.connection.cursor()
for model_name, field_name in self.fields_to_replace():
Model = pool.get(model_name)
table = Model.__table__()
column = Column(table, field_name)
where = column.in_(source_ids)
if transaction.database.has_returning():
returning = [table.id]
else:
cursor.execute(*table.select(table.id, where=where))
ids = [x[0] for x in cursor]
returning = None
cursor.execute(*table.update(
[column],
[destination.id],
where=where,
returning=returning))
if transaction.database.has_returning():
ids = [x[0] for x in cursor]
Model._insert_history(ids)
# delete lots
cursor.execute(*lot.delete(
where=lot.id.in_(source_ids)))
return 'end'
@classmethod
def fields_to_replace(cls):
return [
('stock.move', 'lot'),
('stock.inventory.line', 'lot'),
]
def end(self):
return 'reload'
class LotReplaceAsk(ModelView):
"""Replace Lot Ask"""
__name__ = 'stock.lot.replace.ask'
sources = fields.One2Many('stock.lot', None, 'Source', required=True,
help='The lots to be replaced.')
destination = fields.Many2One('stock.lot', 'Destination', required=True,
domain=[
('id', 'not in', Eval('sources', -1)),
],
depends=['sources'],
help='The lot that replaces.')
@classmethod
def default_sources(cls):
context = Transaction().context
if context.get('active_model') == 'stock.lot':
ids = context.get('active_ids')
if ids:
# remove first of them
ids = ids[1:]
return ids
@classmethod
def default_destination(cls):
context = Transaction().context
if context.get('active_model') == 'stock.lot':
return context.get('active_id')

29
stock.xml Normal file
View file

@ -0,0 +1,29 @@
<?xml version="1.0"?>
<!-- The COPYRIGHT file at the top level of this repository contains the full
copyright notices and license terms. -->
<tryton>
<data>
<!-- Replace -->
<record model="ir.action.wizard" id="wizard_replace">
<field name="name">Replace</field>
<field name="wiz_name">stock.lot.replace</field>
<field name="model">stock.lot</field>
</record>
<record model="ir.action-res.group"
id="wizard_replace-group_stock_admin">
<field name="action" ref="wizard_replace"/>
<field name="group" ref="stock.group_stock_admin"/>
</record>
<record model="ir.action.keyword" id="wizard_replace_keyword1">
<field name="keyword">form_action</field>
<field name="model">stock.lot,-1</field>
<field name="action" ref="wizard_replace"/>
</record>
<record model="ir.ui.view" id="replace_ask_view_form">
<field name="model">stock.lot.replace.ask</field>
<field name="type">form</field>
<field name="name">replace_ask_form</field>
</record>
</data>
</tryton>

View file

@ -0,0 +1,143 @@
======================
Lot Replace Scenario
======================
Imports::
>>> import datetime
>>> from dateutil.relativedelta import relativedelta
>>> from decimal import Decimal
>>> from proteus import Model, Wizard
>>> from trytond.tests.tools import activate_modules
>>> from trytond.modules.company.tests.tools import create_company, \
... get_company
>>> today = datetime.date.today()
Install Stock lot unique::
>>> config = activate_modules('stock_lot_unique')
Create company::
>>> _ = create_company()
>>> company = get_company()
Create customer::
>>> Party = Model.get('party.party')
>>> customer = Party(name='Customer')
>>> customer.save()
Create product::
>>> ProductUom = Model.get('product.uom')
>>> ProductTemplate = Model.get('product.template')
>>> Product = Model.get('product.product')
>>> unit, = ProductUom.find([('name', '=', 'Unit')])
>>> product = Product()
>>> template = ProductTemplate()
>>> template.name = 'Product'
>>> template.default_uom = unit
>>> template.type = 'goods'
>>> template.list_price = Decimal('20')
>>> template.cost_price = Decimal('8')
>>> template.save()
>>> product.template = template
>>> product.save()
Get stock locations::
>>> Location = Model.get('stock.location')
>>> warehouse_loc, = Location.find([('code', '=', 'WH')])
>>> supplier_loc, = Location.find([('code', '=', 'SUP')])
>>> customer_loc, = Location.find([('code', '=', 'CUS')])
>>> output_loc, = Location.find([('code', '=', 'OUT')])
>>> storage_loc, = Location.find([('code', '=', 'STO')])
Create a lot::
>>> Lot = Model.get('stock.lot')
>>> lot1 = Lot(number='L1', product=product)
>>> lot1.save()
Create a second lot::
>>> lot2 = Lot(number='L-1', product=product)
>>> lot2.save()
Create Shipment Out::
>>> ShipmentOut = Model.get('stock.shipment.out')
>>> shipment_out = ShipmentOut()
>>> shipment_out.planned_date = today
>>> shipment_out.customer = customer
>>> shipment_out.warehouse = warehouse_loc
>>> shipment_out.company = company
Add two shipment lines of same product::
>>> StockMove = Model.get('stock.move')
>>> move = StockMove()
>>> shipment_out.outgoing_moves.append(move)
>>> move.product = product
>>> move.uom =unit
>>> move.quantity = 10
>>> move.from_location = output_loc
>>> move.to_location = customer_loc
>>> move.company = company
>>> move.unit_price = Decimal('1')
>>> move.currency = company.currency
>>> move = StockMove()
>>> shipment_out.outgoing_moves.append(move)
>>> move.product = product
>>> move.uom =unit
>>> move.quantity = 4
>>> move.from_location = output_loc
>>> move.to_location = customer_loc
>>> move.company = company
>>> move.unit_price = Decimal('1')
>>> move.currency = company.currency
>>> shipment_out.save()
Set the shipment state to waiting::
>>> shipment_out.click('wait')
>>> len(shipment_out.outgoing_moves)
2
>>> len(shipment_out.inventory_moves)
2
Assign the shipment with 2 lines of 7 products::
>>> for move in shipment_out.inventory_moves:
... move.quantity = 7
>>> shipment_out.click('assign_force')
>>> shipment_out.state
u'assigned'
Set 2 lots::
>>> Lot = Model.get('stock.lot')
>>> shipment_out.inventory_moves[0] = lot1
>>> shipment_out.inventory_moves[1] = lot2
>>> shipment_out.save()
Replace the second by the first lot::
>>> replace = Wizard('stock.lot.replace', models=[lot1,lot2])
>>> replace.form.sources == [lot2]
True
>>> replace.form.destination == lot1
True
>>> replace.execute('replace') # doctest: +IGNORE_EXCEPTION_DETAIL
Traceback (most recent call last):
...
UserWarning: ('UserWarning', ('stock.lot.replace number 2 1', u'Lots have different numbers: L-1 vs L1.', ''))
>>> Model.get('res.user.warning')(user=config.user,
... name='stock.lot.replace number L-1 L1', always=True).save()
>>> replace.execute('replace')
>>> lot1, = Lot.find([])
>>> not StockMove.find([('lot', '=', lot2.id)])
True

View file

@ -2,7 +2,10 @@
# the full copyright notices and license terms.
from decimal import Decimal
import unittest
import doctest
from trytond.tests.test_tryton import ModuleTestCase, with_transaction
from trytond.tests.test_tryton import doctest_teardown
from trytond.tests.test_tryton import doctest_checker
from trytond.tests.test_tryton import suite as test_suite
from trytond.pool import Pool
from trytond.exceptions import UserError
@ -51,4 +54,9 @@ def suite():
suite = test_suite()
suite.addTests(unittest.TestLoader().loadTestsFromTestCase(
StockLotUniqueTestCase))
suite.addTests(doctest.DocFileSuite(
'scenario_lot_replace.rst',
tearDown=doctest_teardown, encoding='utf-8',
checker=doctest_checker,
optionflags=doctest.REPORT_ONLY_FIRST_FAILURE))
return suite

View file

@ -6,3 +6,4 @@ depends:
stock_lot
xml:
stock.xml

View file

@ -0,0 +1,6 @@
<?xml version="1.0"?>
<form>
<field name="sources" string="Lots" colspan="4" widget="many2many"/>
<label name="destination" string="Replace By"/>
<field name="destination"/>
</form>