trytonpsk-stock_co/shipment.py

696 lines
24 KiB
Python
Executable File

# 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 datetime import date, timedelta
from decimal import Decimal
from operator import itemgetter
from sql import Table, Null
from trytond.modules.company import CompanyReport
from trytond.pool import Pool, PoolMeta
from trytond.wizard import (
Wizard, StateTransition, StateView,
Button, StateReport)
from trytond.transaction import Transaction
from trytond.model import ModelView, fields
from trytond.report import Report
from trytond.pyson import Eval
type_shipment = {
'out': 'Envio a Clientes',
'in': 'Envio de Proveedor',
'internal': 'Envio Interno',
}
class ShipmentIn(metaclass=PoolMeta):
'Shipment In'
__name__ = 'stock.shipment.in'
@classmethod
def __setup__(cls):
super(ShipmentIn, cls).__setup__()
cls.reference.states = {
'required': Eval('state').in_(['received']),
}
class ShipmentOut(metaclass=PoolMeta):
'Shipment Out'
__name__ = 'stock.shipment.out'
def get_sale_reference(self, name=None):
for move in self.moves:
if move.origin and str(move.origin).split(',')[0] == 'sale.line':
Sale = Pool().get('sale.sale')
sale = Sale(move.origin.sale.id)
return sale
return None
class InternalShipment(metaclass=PoolMeta):
'Internal Shipment'
__name__ = 'stock.shipment.internal'
@classmethod
def __setup__(cls):
super(InternalShipment, cls).__setup__()
cls.from_location.domain = [
('name', 'ilike', 'ZA%'),
('active', '=', True)
]
cls.to_location.domain = [
('name', 'ilike', 'ZA%'),
('active', '=', True)
]
@classmethod
def wait(cls, shipments):
super(InternalShipment, cls).wait(shipments)
cls.set_employee(shipments, 'shipped_by')
@classmethod
@ModelView.button_action('stock.wizard_shipment_internal_assign')
def assign_wizard(cls, shipments):
super(InternalShipment, cls).assign_wizard(shipments)
for sh in shipments:
cls.assign_lots(sh)
@classmethod
def done(cls, shipments):
super(InternalShipment, cls).done(shipments)
cls.set_employee(shipments, 'done_by')
@classmethod
def set_employee(cls, shipments, field):
employee_id = Transaction().context.get('employee')
if employee_id:
cls.write(shipments, {field: employee_id})
@classmethod
def assign_lots(cls, shipment):
"""
Look for the oldest lot or the one closest to the expiration
date and try assign it to shipment moves
"""
Move = Pool().get('stock.move')
Lot = Pool().get('stock.lot')
def _get_lots(product_id):
locations_ids = {'locations': [shipment.from_location.id]}
order = []
fields = Lot.fields_get(fields_names=['expiration_date'])
if 'expiration_date' in fields.keys():
order.append(('expiration_date', 'ASC NULLS LAST'))
order.append(('create_date', 'ASC'))
with Transaction().set_context(locations_ids):
dom = [
('product', '=', product_id),
('active', '=', True),
('quantity', '>', 0),
]
lots = Lot.search(dom, order=order)
return lots
for move in shipment.moves:
if move.product.lot_is_required:
lots_list = _get_lots(move.product.id)
balance_qty = move.quantity
for lt in lots_list:
if lt.quantity >= balance_qty:
move.lot = lt.id
move.save()
break
else:
balance_qty = balance_qty - lt.quantity
move.quantity = lt.quantity
move.lot = lt.id
move.save()
if balance_qty:
move, = Move.create([{
'quantity': balance_qty,
'internal_quantity': balance_qty,
'product': move.product.id,
'lot': lt.id,
'from_location': move.from_location.id,
'to_location': move.to_location.id,
'shipment': str(shipment),
'uom': move.uom.id,
'company': move.company.id,
'state': move.state,
}])
@fields.depends('effective_date', 'from_location', 'to_location')
def on_change_with_effective_start_date(self):
if self.effective_date:
return self.effective_date
@classmethod
def create(cls, vlist):
shipments = super().create(vlist)
cls._set_move_effective_date(shipments)
return shipments
@classmethod
def write(cls, *args):
super().write(*args)
cls._set_move_effective_date(sum(args[::2], []))
@property
def _move_effective_date(self):
'''
Return the effective date for incoming moves and inventory_moves
'''
return self.effective_start_date, self.effective_date
@classmethod
def _set_move_effective_date(cls, shipments):
'''
Set effective date of moves for the shipments
'''
Move = Pool().get('stock.move')
to_write = []
for shipment in shipments:
dates = shipment._move_effective_date
outgoing_date, incoming_date = dates
outgoing_moves = [m for m in shipment.outgoing_moves
if (m.state not in ('assigned', 'done', 'cancelled')
and m.effective_date != outgoing_date)]
if outgoing_moves:
to_write.append(outgoing_moves)
to_write.append({
'effective_date': outgoing_date,
})
if shipment.transit_location:
incoming_moves = [m for m in shipment.incoming_moves
if (m.state not in ('assigned', 'done', 'cancelled')
and m.effective_date != incoming_date)]
if incoming_moves:
to_write.append(incoming_moves)
to_write.append({
'effective_date': incoming_date,
})
if to_write:
Move.write(*to_write)
def load_current_stock(self):
pool = Pool()
Internal = pool.get('stock.shipment.internal')
Product = pool.get('product.product')
moves_target = []
ctx = {
'locations': [self.from_location.id],
'stock_date_end': self.planned_date,
}
with Transaction().set_context(ctx):
products = Product.search([
('quantity', '>', 0)
])
for product in products:
if product.quantity <= 0:
continue
moves_target.append({
'product': product.id,
'uom': product.default_uom.id,
'quantity': product.quantity,
'internal_quantity': product.quantity,
'from_location': self.from_location.id,
'to_location': self.to_location.id,
'planned_date': self.planned_date or self.effective_date,
'effective_date': self.planned_date or self.effective_date,
'state': 'draft',
})
Internal.write([self], {
'moves': [('create', moves_target)],
})
return 'end'
class CreateInternalShipmentStart(ModelView):
'Create Internal Shipment Start'
__name__ = 'stock_co.create_internal_shipment.start'
from_location = fields.Many2One('stock.location', "From Location",
required=True, domain=[
('type', 'in', [
'view', 'storage', 'lost_found']),
])
to_location = fields.Many2One('stock.location', "To Location",
required=True, domain=[
('type', 'in', [
'view', 'storage', 'lost_found']),
])
class CreateInternalShipment(Wizard):
'Create Internal Shipment'
__name__ = 'stock_co.create_internal_shipment'
start = StateView('stock_co.create_internal_shipment.start',
'stock_co.create_internal_shipment_start_view_form', [
Button('Cancel', 'end', 'tryton-cancel'),
Button('Ok', 'accept', 'tryton-ok', default=True),
])
accept = StateTransition()
def transition_accept(self):
pool = Pool()
Internal = pool.get('stock.shipment.internal')
ShipmentSupplier = pool.get('stock.shipment.in')
shipment_id = Transaction().context.get('active_id')
shipment_supplier = ShipmentSupplier(shipment_id)
moves_target = []
today = date.today()
for move in shipment_supplier.incoming_moves:
moves_target.append({
'product': move.product.id,
'uom': move.product.default_uom.id,
'quantity': move.quantity,
'internal_quantity': move.quantity,
'from_location': self.start.from_location.id,
'to_location': self.start.to_location.id,
'planned_date': today,
'effective_date': today,
'state': 'draft',
})
data = {
'company': shipment_supplier.company.id,
'effective_date': today,
'planned_date': today,
'effective_start_date': today,
'planned_start_date': today,
'from_location': self.start.from_location.id,
'to_location': self.start.to_location.id,
'state': 'draft',
'moves': [('create', moves_target)],
}
Internal.create([data])
return 'end'
class ShipmentInReturnReport(CompanyReport):
'Shipment In Return Report'
__name__ = 'stock.shipment.in.return.report'
class ShipmentOutForceDraft(Wizard):
'Shipment Out Force Draft'
__name__ = 'stock.shipment.out.force_draft'
start_state = 'force_draft'
force_draft = StateTransition()
def transition_force_draft(self):
id_ = Transaction().context['active_id']
Shipment = Pool().get('stock.shipment.out')
stock_move = Table('stock_move')
if id_:
shipment = Shipment(id_)
cursor = Transaction().connection.cursor()
for rec in shipment.inventory_moves:
cursor.execute(*stock_move.delete(
where=stock_move.id == rec.id)
)
for rec in shipment.outgoing_moves:
cursor.execute(*stock_move.update(
columns=[stock_move.state, stock_move.effective_date],
values=['draft', Null],
where=stock_move.id == rec.id)
)
shipment.state = 'draft'
shipment.save()
return 'end'
class ShipmentInternalForceDraft(Wizard):
'Shipment Internal Force Draft'
__name__ = 'stock.shipment.internal.force_draft'
start_state = 'force_draft'
force_draft = StateTransition()
def transition_force_draft(self):
id_ = Transaction().context['active_id']
Shipment = Pool().get('stock.shipment.internal')
stock_move = Table('stock_move')
if id_:
shipment = Shipment(id_)
if shipment.effective_date > date.today() - timedelta(days=30):
cursor = Transaction().connection.cursor()
for rec in shipment.moves:
cursor.execute(*stock_move.update(
columns=[stock_move.state],
values=['draft'],
where=stock_move.id == rec.id)
)
shipment.state = 'draft'
shipment.save()
return 'end'
class ShipmentInForceDraft(Wizard):
'Shipment In Force Draft'
__name__ = 'stock.shipment.in.force_draft'
start_state = 'force_draft'
force_draft = StateTransition()
def transition_force_draft(self):
shipment = Table('stock_shipment_in')
move = Table('stock_move')
id_shipment = Transaction().context['active_id']
cursor = Transaction().connection.cursor()
if not id_shipment:
return 'end'
cursor.execute(*shipment.update(
columns=[shipment.state],
values=['draft'],
where=shipment.id == id_shipment)
)
pool = Pool()
ShipmentIn = pool.get('stock.shipment.in')
shipmentin = ShipmentIn(id_shipment)
moves_ids = [m.id for m in shipmentin.inventory_moves]
if moves_ids:
cursor.execute(*move.delete(
where=move.id.in_(moves_ids))
)
moves_ids = [m.id for m in shipmentin.incoming_moves]
if moves_ids:
cursor.execute(*move.update(
columns=[move.state],
values=['draft'],
where=move.id.in_(moves_ids))
)
return 'end'
class ShipmentInReturnForceDraft(Wizard):
'Shipment In Return Force Draft'
__name__ = 'stock.shipment.in.return.force_draft'
start_state = 'force_draft'
force_draft = StateTransition()
def transition_force_draft(self):
shipment = Table('stock_shipment_in_return')
move = Table('stock_move')
id_shipment = Transaction().context['active_id']
cursor = Transaction().connection.cursor()
if not id_shipment:
return 'end'
cursor.execute(*shipment.update(
columns=[shipment.state],
values=['draft'],
where=shipment.id == id_shipment)
)
pool = Pool()
ShipmentIn = pool.get('stock.shipment.in.return')
shipment_in = ShipmentIn(id_shipment)
moves_ids = [m.id for m in shipment_in.moves]
if moves_ids:
cursor.execute(*move.update(
columns=[move.state],
values=['draft'],
where=move.id.in_(moves_ids))
)
return 'end'
class ShipmentDetailedStart(ModelView):
'Shipment Detailed Start'
__name__ = 'stock.shipment.shipment_detailed.start'
company = fields.Many2One('company.company', 'Company', required=True)
start_date = fields.Date('Start Date', required=True)
end_date = fields.Date('End Date', required=True)
type_shipment = fields.Selection([
('in', 'Supplier'),
('out', 'Customer'),
('internal', 'Internal'),
], 'Type Shipment', required=True)
grouped = fields.Boolean('Grouped')
from_locations = fields.Many2Many('stock.location', None, None,
'From Location', domain=[
('name', 'ilike', 'ZA%'),
('active', '=', True)
])
to_locations = fields.Many2Many('stock.location', None, None, 'To Location',
domain=[
('name', 'ilike', 'ZA%'),
('active', '=', True)
])
@staticmethod
def default_company():
return Transaction().context.get('company')
class ShipmentDetailed(Wizard):
'Shipment Detailed'
__name__ = 'stock.shipment.shipment_detailed'
start = StateView('stock.shipment.shipment_detailed.start',
'stock_co.print_shipment_detailed_start_view_form', [
Button('Cancel', 'end', 'tryton-cancel'),
Button('Print', 'print_', 'tryton-ok', default=True),
])
print_ = StateReport('stock.shipment.shipment_detailed.report')
def do_print_(self, action):
from_locations = None
to_locations = None
if self.start.from_locations:
from_locations = [n.id for n in self.start.from_locations]
if self.start.to_locations:
to_locations = [n.id for n in self.start.to_locations]
data = {
'company': self.start.company.id,
'start_date': self.start.start_date,
'end_date': self.start.end_date,
'type_shipment': self.start.type_shipment,
'grouped': self.start.grouped,
'from_locations': from_locations,
'to_locations': to_locations
}
return action, data
def transition_print_(self):
return 'end'
class ShipmentDetailedReport(Report):
'Shipment Detailed Report'
__name__ = 'stock.shipment.shipment_detailed.report'
@classmethod
def get_context(cls, records, header, data):
report_context = super().get_context(records, header, data)
pool = Pool()
company = Transaction().context.get('company.rec_name')
type_shipment_ = data['type_shipment']
model = 'stock.shipment.' + type_shipment_
ModelShipment = pool.get(model)
Move = pool.get('stock.move')
Product = pool.get('product.product')
dom_shipment = [
('company', '=', data['company']),
('effective_date', '>=', data['start_date']),
('effective_date', '<=', data['end_date']),
('state', 'not in', ['cancelled', 'draft'])
]
if data['from_locations']:
dom_shipment.append(('from_location', 'in', data['from_locations']))
if data['to_locations']:
dom_shipment.append(('to_location', 'in', data['to_locations']))
fields_names = ['id']
shipments = ModelShipment.search_read(
dom_shipment,
fields_names=fields_names,
order=[('effective_date', 'ASC')]
)
shipments_id = [model + ',' + str(sh['id']) for sh in shipments]
fields_names = [
'product.account_category.name', 'product.name', 'product.code',
'product.cost_price', 'quantity', 'to_location.name',
'from_location.name', 'shipment.reference',
'effective_date', 'shipment.number'
]
fields = ModelShipment.fields_get(fields_names=[
'operation_center', 'customer', 'supplier',
'incoming_moves', 'analytic_account'])
if 'operation_center' in fields.keys():
fields_names.append('shipment.operation_center.rec_name')
if 'analytic_account' in fields.keys():
fields_names.append('shipment.analytic_account.rec_name')
if type_shipment_ == 'in':
fields_names.append('shipment.supplier.name')
elif type_shipment_ == 'out':
fields_names.append('shipment.customer.name')
moves = Move.search_read(
('shipment', 'in', shipments_id),
fields_names=fields_names,
order=[('to_location', 'DESC'), ('create_date', 'ASC')]
)
dgetter = itemgetter('product.', 'quantity')
product_browse = Product.browse
for m in moves:
product, quantity = dgetter(m)
product_, = product_browse([product['id']])
try:
oc = m['shipment.']['operation_center.']['rec_name']
except Exception:
oc = ''
try:
analytic = m['shipment.']['analytic_account.']['rec_name']
except Exception:
analytic = ''
if type_shipment_ == 'in':
party = m['shipment.']['supplier.']['name']
elif type_shipment_ == 'out':
party = m['shipment.']['customer.']['name']
else:
party = ''
cost_price = product['cost_price']
category = product.get('account_category.', '')
if category:
category = category['name']
category_ad = ''
if product_.categories:
category_ad = product_.categories[0].name
value = {
'party': party,
'oc': oc,
'analytic': analytic,
'product': product['name'],
'code': product['code'],
'cost_price': cost_price,
'category': category,
'category_ad': category_ad,
'cost_base': Decimal(str(round(float(cost_price) * quantity, 2))),
}
try:
value['cost_unit_w_tax'] = float(product_.cost_price_taxed)
value['cost_w_tax'] = float(
product_.cost_price_taxed) * quantity
value['last_cost'] = product_.last_cost
except Exception:
value['cost_w_tax'] = 0
value['cost_unit_w_tax'] = 0
value['last_cost'] = 0
try:
m.update(product_.attributes)
except Exception:
pass
try:
value['price_w_tax'] = float(
product_.sale_price_w_tax) * quantity
except Exception:
value['price_w_tax'] = 0
try:
value['section'] = product_.section.name
value['conservation'] = product_.conservation.name
except Exception:
value['conservation'] = None
value['section'] = None
m.update(value)
if not data['grouped']:
report_context['records'] = moves
else:
records = {}
for m in moves:
location_id = str(m['to_location.']['id'])
product_id = str(m['product.']['id'])
key = location_id + '_' + product_id
try:
records[key]['cost_w_tax'] += m['cost_w_tax']
records[key]['price_w_tax'] += m['price_w_tax']
records[key]['quantity'] += m['quantity']
records[key]['cost_base'] += m['cost_base']
records[key]['last_cost'] = m['last_cost']
except Exception:
records[key] = m
records[key]['shipment.']['reference'] = ''
records[key]['effective_date'] = ''
report_context['records'] = records.values()
report_context['company'] = company
report_context['Decimal'] = Decimal
report_context['kind'] = type_shipment[data['type_shipment']]
return report_context
class Assign(metaclass=PoolMeta):
"Assign Shipment"
__name__ = 'stock.shipment.assign'
split = StateTransition()
@classmethod
def __setup__(cls):
super(Assign, cls).__setup__()
cls.partial.buttons.append(Button('Split', 'split', 'tryton-ok'))
def transition_split(self):
self.split_shipment()
return 'end'
def split_shipment(self):
pool = Pool()
Move = pool.get('stock.move')
Shipment = pool.get(Transaction().context.get('active_model'))
shipment_ = Shipment(Transaction().context.get('active_id'))
number_reference = shipment_.number
if Shipment.__name__ == 'stock.shipment.out':
Move.draft(shipment_.inventory_moves)
shipment, = Shipment.copy([shipment_], default={'moves': None})
Shipment.write([shipment], {'reference': number_reference})
move_to_write = []
for m in self.partial.moves:
move, = Move.copy([m.origin], default={'quantity': m.quantity})
Move.write([m], {'origin': str(m)})
move_to_write.append(move)
Move.write(list(self.partial.moves) + move_to_write, {'shipment': str(shipment)})
inventory_ids = []
for m in shipment_.inventory_moves:
Move.write([m.origin], {'quantity': m.quantity})
inventory_ids.append(m.origin.id)
for m in shipment_.outgoing_moves:
if m.id not in inventory_ids:
Move.delete([m])
else:
shipment, = Shipment.copy([shipment_], default={'moves': None})
Shipment.write([shipment], {'reference': number_reference})
Move.write(list(self.partial.moves), {'shipment': str(shipment)})
return 'end'
class ShipmentInternalLoadStock(Wizard):
'Shipment Internal Load Stock'
__name__ = 'stock_co.shipment_internal_load_stock'
start_state = 'create_moves'
create_moves = StateTransition()
def transition_create_moves(self):
Internal = Pool().get('stock.shipment.internal')
active_id = Transaction().context['active_id']
internal = Internal(active_id)
if not internal.moves:
internal.load_current_stock()
return 'end'