mirror of
https://gitlab.com/datalifeit/trytond-stock_lot_unique
synced 2023-12-14 05:12:57 +01:00
parent
cfba67818b
commit
14d96a5505
|
@ -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')
|
||||
|
|
60
locale/es.po
60
locale/es.po
|
@ -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
145
stock.py
|
@ -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
29
stock.xml
Normal 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>
|
143
tests/scenario_lot_replace.rst
Normal file
143
tests/scenario_lot_replace.rst
Normal 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
|
|
@ -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
|
||||
|
|
|
@ -6,3 +6,4 @@ depends:
|
|||
stock_lot
|
||||
|
||||
xml:
|
||||
stock.xml
|
||||
|
|
6
view/replace_ask_form.xml
Normal file
6
view/replace_ask_form.xml
Normal 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>
|
Loading…
Reference in a new issue