# 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'