568 lines
22 KiB
Python
568 lines
22 KiB
Python
# 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 time
|
|
|
|
from trytond.i18n import gettext
|
|
from trytond.model import fields, ModelView, ModelSQL, Workflow
|
|
from trytond.pool import Pool, PoolMeta
|
|
from trytond.wizard import Wizard, StateView, StateTransition, Button
|
|
from trytond.transaction import Transaction
|
|
from trytond.pyson import Eval
|
|
from trytond.exceptions import UserWarning, UserError
|
|
from sql import Table
|
|
|
|
|
|
class ShipmentAdditional(ModelSQL, ModelView):
|
|
"Shipment Additional"
|
|
__name__ = "stock.shipment.additional"
|
|
shipment = fields.Many2One('stock.shipment.out', 'Shipment Out',
|
|
ondelete='CASCADE')
|
|
shipment_return = fields.Many2One('stock.shipment.out.return',
|
|
'Shipment Out Return', ondelete='CASCADE')
|
|
product = fields.Many2One('product.product', 'Product', domain=[
|
|
('template.salable', '=', True)
|
|
], required=True)
|
|
quantity = fields.Float('Quantity', required=True)
|
|
unit_price = fields.Numeric('Unit Price', digits=(16, 2), required=True)
|
|
notes = fields.Char('Notes', required=True)
|
|
|
|
|
|
class Move(metaclass=PoolMeta):
|
|
__name__ = 'stock.move'
|
|
|
|
@classmethod
|
|
def check_origin(cls, moves, types=None):
|
|
# cls.check_origin(moves)
|
|
pass
|
|
|
|
@fields.depends('from_location', 'to_location')
|
|
def on_change_with_unit_price_required(self, name=None):
|
|
from_type = self.from_location.type if self.from_location else None
|
|
to_type = self.to_location.type if self.to_location else None
|
|
# res = super(Move, self).on_change_with_assignation_required(name=name)
|
|
if from_type == 'customer' and to_type == 'storage':
|
|
return True
|
|
if from_type == 'supplier' and to_type == 'storage':
|
|
return True
|
|
if from_type == 'production':
|
|
return True
|
|
if from_type == 'storage' and to_type == 'customer':
|
|
return True
|
|
if from_type == 'storage' and to_type == 'supplier':
|
|
return True
|
|
return False
|
|
|
|
|
|
class ShipmentOut(metaclass=PoolMeta):
|
|
__name__ = 'stock.shipment.out'
|
|
vehicle = fields.Char('Vehicle')
|
|
driver = fields.Char('Driver')
|
|
location_customer = fields.Many2One('stock.location', 'Customer Location',
|
|
domain=[('type', '=', 'customer')]
|
|
)
|
|
additionals = fields.One2Many('stock.shipment.additional', 'shipment',
|
|
'Shipment Additionals')
|
|
ship_time = fields.Time('Ship Time')
|
|
|
|
@fields.depends('customer', 'location_customer')
|
|
def on_change_with_customer_location(self, name=None):
|
|
customer_id = super(ShipmentOut, self).on_change_with_customer_location(name=None)
|
|
if self.location_customer:
|
|
return self.location_customer.id
|
|
else:
|
|
return customer_id
|
|
|
|
@classmethod
|
|
def draft(cls, shipments):
|
|
stock_move = Table('stock_move')
|
|
Move = Pool().get('stock.move')
|
|
cursor = Transaction().connection.cursor()
|
|
|
|
for s in shipments:
|
|
for m in s.moves:
|
|
cursor.execute(*stock_move.update(
|
|
columns=[stock_move.state],
|
|
values=['draft'],
|
|
where=stock_move.id == m.id)
|
|
)
|
|
for m in s.outgoing_moves:
|
|
Move.delete([m])
|
|
s.write([s], {'state': 'draft'})
|
|
|
|
|
|
class ShipmentOutForceDraft(Wizard):
|
|
'ShipmentOut 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']
|
|
|
|
ShipmentOut = Pool().get('stock.shipment.out')
|
|
if id_:
|
|
shipments = ShipmentOut.browse([id_])
|
|
ShipmentOut.draft(shipments)
|
|
return 'end'
|
|
|
|
|
|
class ShipmentOutReturn(metaclass=PoolMeta):
|
|
__name__ = 'stock.shipment.out.return'
|
|
vehicle = fields.Char('Vehicle')
|
|
driver = fields.Char('Driver')
|
|
location_customer = fields.Many2One('stock.location', 'Customer Location',
|
|
domain=[('type', '=', 'customer')]
|
|
)
|
|
additionals = fields.One2Many('stock.shipment.additional', 'shipment_return',
|
|
'Shipment Additionals')
|
|
ship_time = fields.Time('Ship Time')
|
|
|
|
@classmethod
|
|
def __setup__(cls):
|
|
super(ShipmentOutReturn, cls).__setup__()
|
|
|
|
@fields.depends('customer', 'location_customer')
|
|
def on_change_with_customer_location(self, name=None):
|
|
customer_id = super(ShipmentOutReturn, self).on_change_with_customer_location(name=None)
|
|
if self.location_customer:
|
|
return self.location_customer.id
|
|
else:
|
|
return customer_id
|
|
|
|
@classmethod
|
|
def draft(cls, shipments):
|
|
stock_move = Table('stock_move')
|
|
Move = Pool().get('stock.move')
|
|
cursor = Transaction().connection.cursor()
|
|
|
|
for s in shipments:
|
|
for m in s.moves:
|
|
cursor.execute(*stock_move.update(
|
|
columns=[stock_move.state],
|
|
values=['draft'],
|
|
where=stock_move.id == m.id)
|
|
)
|
|
for m in s.inventory_moves:
|
|
Move.delete([m])
|
|
s.write([s], {'state': 'draft'})
|
|
|
|
@classmethod
|
|
@Workflow.transition('received')
|
|
def receive(cls, shipments):
|
|
for shipment in shipments:
|
|
for move in shipment.incoming_moves:
|
|
quantity = shipment.validate_product_quantity(move)
|
|
if quantity < move.quantity:
|
|
raise UserError(gettext(
|
|
'rental.msg_product_not_stock', product=move.product.name
|
|
))
|
|
# raise UserError(
|
|
# 'rental.msg_product_not_stock, ' + move.product.name
|
|
# )
|
|
super(ShipmentOutReturn, cls).receive(shipments)
|
|
|
|
def validate_product_quantity(self, move):
|
|
res = 0
|
|
location_id = move.from_location.id
|
|
if move.product:
|
|
stock_context = {
|
|
'stock_date_end': move.effective_date or move.planned_date,
|
|
'locations': [location_id],
|
|
}
|
|
with Transaction().set_context(stock_context):
|
|
try:
|
|
res_dict = move.product._get_quantity([move.product], 'quantity', [location_id], grouping_filter=([move.product.id],))
|
|
if res_dict.get(move.product.id):
|
|
res += res_dict[move.product.id]
|
|
except AttributeError as error:
|
|
print(error)
|
|
return res
|
|
|
|
@staticmethod
|
|
def _get_inventory_moves(incoming_move):
|
|
pool = Pool()
|
|
Move = pool.get('stock.move')
|
|
if incoming_move.quantity <= 0.0:
|
|
return
|
|
move = Move()
|
|
move.product = incoming_move.product
|
|
move.uom = incoming_move.uom
|
|
move.quantity = incoming_move.quantity
|
|
move.from_location = incoming_move.to_location
|
|
move.to_location = incoming_move.shipment.warehouse_storage
|
|
move.state = Move.default_state()
|
|
move.unit_price = incoming_move.unit_price
|
|
# Product will be considered in stock only when the inventory
|
|
# move will be made:
|
|
move.planned_date = None
|
|
move.company = incoming_move.company
|
|
return move
|
|
|
|
|
|
class Location(metaclass=PoolMeta):
|
|
__name__ = 'stock.location'
|
|
|
|
@classmethod
|
|
def __setup__(cls):
|
|
super(Location, cls).__setup__()
|
|
cls.address.states.update(
|
|
{'invisible': ~Eval('type').in_(['warehouse', 'customer'])},
|
|
)
|
|
|
|
|
|
class CreateSalesFromMovesStart(ModelView):
|
|
'Create Sales From Moves Start'
|
|
__name__ = 'rental.create_sales_from_moves.start'
|
|
start_date = fields.Date('Start Date', required=True)
|
|
end_date = fields.Date('End Date', required=True)
|
|
party = fields.Many2One('party.party', 'Party')
|
|
company = fields.Many2One('company.company', 'Company', required=True)
|
|
description = fields.Char('Description', required=True)
|
|
|
|
@staticmethod
|
|
def default_company():
|
|
return Transaction().context.get('company')
|
|
|
|
|
|
class CreateSalesFromMoves(Wizard):
|
|
'Create Sales From Moves'
|
|
__name__ = 'rental.create_sales_from_moves'
|
|
start = StateView('rental.create_sales_from_moves.start',
|
|
'rental.create_sales_from_moves_view_form', [
|
|
Button('Cancel', 'end', 'tryton-cancel'),
|
|
Button('Ok', 'accept', 'tryton-ok', default=True),
|
|
])
|
|
accept = StateTransition()
|
|
|
|
def _get_move_by_products(self, moves):
|
|
group = {}
|
|
for m in moves:
|
|
try:
|
|
group[m.product.id].append(m)
|
|
except:
|
|
group[m.product.id] = []
|
|
group[m.product.id].append(m)
|
|
return group
|
|
|
|
def _get_move_line_start(self, location_id, moves, date_):
|
|
res = {}
|
|
qties = []
|
|
unit_price = 0
|
|
for m in moves:
|
|
if m.to_location.id == location_id:
|
|
qty = m.quantity
|
|
else:
|
|
qty = -1 * (m.quantity)
|
|
qties.append(qty)
|
|
unit_price = m.unit_price
|
|
balance = sum(qties)
|
|
if balance:
|
|
res = {
|
|
'date': date_,
|
|
'qty': balance,
|
|
'total': balance,
|
|
'product': moves[0].product,
|
|
'shipment': None,
|
|
'unit_price': unit_price
|
|
}
|
|
return res
|
|
|
|
def _get_move_line_current(self, location_id, moves):
|
|
res = []
|
|
total = 0
|
|
for m in moves:
|
|
if m.to_location.id == location_id:
|
|
qty = m.quantity
|
|
else:
|
|
qty = -1 * (m.quantity)
|
|
total += qty
|
|
|
|
res.append({
|
|
'date': m.effective_date,
|
|
'ship_time': m.shipment.ship_time or time(6, 0),
|
|
'qty': qty,
|
|
'total': total,
|
|
'product': m.product,
|
|
'shipment': m.shipment.number,
|
|
'unit_price': m.unit_price
|
|
})
|
|
return res
|
|
|
|
def transition_accept(self):
|
|
pool = Pool()
|
|
Sale = pool.get('sale.sale')
|
|
Party = pool.get('party.party')
|
|
SaleLine = pool.get('sale.line')
|
|
Move = pool.get('stock.move')
|
|
Location = pool.get('stock.location')
|
|
ShipmentOut = pool.get('stock.shipment.out')
|
|
ShipmentOutReturn = pool.get('stock.shipment.out.return')
|
|
|
|
domain_shipments = [
|
|
('location_customer', '!=', None),
|
|
('state', '=', 'done'),
|
|
]
|
|
|
|
if self.start.party:
|
|
domain_shipments.append(
|
|
('customer', '=', self.start.party.id)
|
|
)
|
|
|
|
shipments = ShipmentOut.search(domain_shipments)
|
|
|
|
parties_locations = {}
|
|
twelve = time(12, 0)
|
|
for s in shipments:
|
|
try:
|
|
parties_locations[s.customer.id].append(s.location_customer.id)
|
|
except:
|
|
parties_locations[s.customer.id] = [s.location_customer.id]
|
|
|
|
shipments = ShipmentOutReturn.search(domain_shipments)
|
|
for s in shipments:
|
|
try:
|
|
parties_locations[s.customer.id].append(s.location_customer.id)
|
|
except:
|
|
parties_locations[s.customer.id] = [s.location_customer.id]
|
|
|
|
for party_id, location_ids in parties_locations.items():
|
|
locations = set(location_ids)
|
|
party = Party(party_id)
|
|
for loc_id in locations:
|
|
location, = Location.browse([loc_id])
|
|
start_shipments = ShipmentOut.search([
|
|
('effective_date', '<', self.start.start_date),
|
|
('customer', '=', party_id),
|
|
('location_customer', '=', loc_id),
|
|
('state', '=', 'done'),
|
|
], order=[('effective_date', 'ASC')])
|
|
|
|
start_shipments_return = ShipmentOutReturn.search([
|
|
('effective_date', '<', self.start.start_date),
|
|
('customer', '=', party_id),
|
|
('location_customer', '=', loc_id),
|
|
('state', '=', 'done'),
|
|
], order=[('effective_date', 'ASC')])
|
|
|
|
target_start_moves_ids = []
|
|
|
|
for s in start_shipments:
|
|
target_start_moves_ids.extend([mov.id for mov in s.outgoing_moves])
|
|
|
|
for s in start_shipments_return:
|
|
target_start_moves_ids.extend([mov.id for mov in s.incoming_moves])
|
|
|
|
start_moves = Move.search([
|
|
('effective_date', '<', self.start.start_date),
|
|
('id', 'in', target_start_moves_ids),
|
|
('state', '=', 'done'),
|
|
], order=[('effective_date', 'ASC')])
|
|
|
|
shipments = ShipmentOut.search([
|
|
('effective_date', '>=', self.start.start_date),
|
|
('effective_date', '<=', self.start.end_date),
|
|
('location_customer', '=', loc_id),
|
|
('customer', '=', party_id),
|
|
('state', '=', 'done'),
|
|
], order=[('effective_date', 'ASC')])
|
|
|
|
shipments_return = ShipmentOutReturn.search([
|
|
('effective_date', '<=', self.start.end_date),
|
|
('effective_date', '>=', self.start.start_date),
|
|
('location_customer', '=', loc_id),
|
|
('customer', '=', party_id),
|
|
('state', '=', 'done'),
|
|
], order=[('effective_date', 'ASC')])
|
|
|
|
target_moves_ids = []
|
|
products_to_add = []
|
|
for s in shipments:
|
|
target_moves_ids.extend([mov.id for mov in s.outgoing_moves])
|
|
products_to_add.extend(s.additionals)
|
|
|
|
for s in shipments_return:
|
|
target_moves_ids.extend([mov.id for mov in s.incoming_moves])
|
|
products_to_add.extend(s.additionals)
|
|
|
|
moves = Move.search([
|
|
('id', 'in', target_moves_ids),
|
|
('product.template.leasable', '=', True),
|
|
('state', '=', 'done'),
|
|
], order=[('effective_date', 'ASC')])
|
|
|
|
if not start_moves and not moves:
|
|
continue
|
|
|
|
start_group = self._get_move_by_products(start_moves)
|
|
current_qty_by_pdt = {}
|
|
for key, _moves in start_group.items():
|
|
current_qty_by_pdt[key] = [self._get_move_line_start(
|
|
loc_id, _moves, self.start.start_date
|
|
)]
|
|
|
|
current_group = self._get_move_by_products(moves)
|
|
for key, _moves in current_group.items():
|
|
if key not in current_qty_by_pdt.keys():
|
|
current_qty_by_pdt[key] = []
|
|
|
|
current_qty_by_pdt[key].extend(self._get_move_line_current(
|
|
loc_id, _moves
|
|
))
|
|
|
|
lines_to_create = []
|
|
|
|
for product_id, values in current_qty_by_pdt.items():
|
|
len_values = len(values)
|
|
total_qty = 0
|
|
for i in range(len_values):
|
|
value = values.pop(0)
|
|
if not value:
|
|
continue
|
|
next_ship_time = time(18, 0)
|
|
if values:
|
|
next_date = values[0]['date']
|
|
if values[0].get('ship_time'):
|
|
next_ship_time = values[0]['ship_time']
|
|
else:
|
|
next_date = self.start.end_date
|
|
|
|
factor = (next_date - value['date']).days
|
|
if next_ship_time > twelve:
|
|
factor = factor + 1
|
|
|
|
if value.get('ship_time') and value['ship_time'] > twelve and next_ship_time > twelve:
|
|
factor = factor - 1
|
|
|
|
qty = round(value['qty'], 2)
|
|
if not lines_to_create:
|
|
new_line = {
|
|
'factor': factor,
|
|
'date': value['date'],
|
|
'product': value['product'],
|
|
'move_qty': qty,
|
|
'stock_balance': value['qty'],
|
|
'shipment': value['shipment'],
|
|
'unit_price': value['unit_price'],
|
|
}
|
|
total_qty = value['qty']
|
|
else:
|
|
total_qty += value['qty']
|
|
new_line = {
|
|
'factor': factor,
|
|
'date': value['date'],
|
|
'product': value['product'],
|
|
'move_qty': qty,
|
|
'stock_balance': total_qty,
|
|
'shipment': value['shipment'],
|
|
'unit_price': value['unit_price'],
|
|
}
|
|
lines_to_create.append(new_line)
|
|
|
|
if not lines_to_create:
|
|
continue
|
|
|
|
lines = []
|
|
for line in lines_to_create:
|
|
taxes_ids = self.get_taxes(party, line['product'])
|
|
unit_price = line['unit_price']
|
|
if not unit_price:
|
|
unit_price = round(line['product'].template.list_price, 2)
|
|
|
|
factor = line['factor']
|
|
if line['stock_balance'] == 0:
|
|
factor = 0
|
|
|
|
stock_balance = line['stock_balance']
|
|
move_qty = line['move_qty']
|
|
qty = round(line['factor'] * stock_balance, 2)
|
|
if int(stock_balance) == 0 and move_qty == 0:
|
|
continue
|
|
|
|
new_line = {
|
|
'type': 'line',
|
|
'unit': line['product'].template.default_uom.id,
|
|
'stock_qty': stock_balance,
|
|
'factor': factor,
|
|
'quantity': qty,
|
|
'unit_price': unit_price,
|
|
'product': line['product'].id,
|
|
'description': line['product'].name,
|
|
'move_qty': line['move_qty'],
|
|
'shipment_number': line['shipment'],
|
|
'shipment_date': line['date'],
|
|
'taxes': [('add', taxes_ids)]
|
|
}
|
|
lines.append(new_line)
|
|
|
|
for extra in products_to_add:
|
|
if not extra.product:
|
|
continue
|
|
taxes_ids = self.get_taxes(party, extra.product)
|
|
if extra.unit_price:
|
|
unit_price = round(extra.unit_price, 2)
|
|
else:
|
|
unit_price = round(extra.product.template.list_price, 2)
|
|
shipment_ = extra.shipment if extra.shipment else extra.shipment_return
|
|
new_line = {
|
|
'type': 'line',
|
|
'unit': extra.product.template.default_uom.id,
|
|
'quantity': extra.quantity,
|
|
'unit_price': unit_price,
|
|
'product': extra.product.id,
|
|
'description': extra.notes or extra.product.name,
|
|
'shipment_number': shipment_.number,
|
|
'shipment_date': shipment_.effective_date,
|
|
'taxes': [('add', taxes_ids)]
|
|
}
|
|
lines.append(new_line)
|
|
|
|
if lines:
|
|
sale, = Sale.create([{
|
|
'party': party_id,
|
|
'company': self.start.company.id,
|
|
'sale_date': self.start.end_date,
|
|
'price_list': None,
|
|
'state': 'draft',
|
|
'reference': location.name,
|
|
'currency': self.start.company.currency.id,
|
|
'invoice_address': Party.address_get(party, type='invoice'),
|
|
'shipment_address': Party.address_get(party, type='delivery'),
|
|
'description': self.start.description,
|
|
'shipment_method': 'manual',
|
|
'lines': [('create', lines)],
|
|
}])
|
|
return 'end'
|
|
|
|
def get_taxes(self, party, product):
|
|
taxes = []
|
|
pattern = {}
|
|
for tax in product.customer_taxes_used:
|
|
if party.customer_tax_rule:
|
|
tax_ids = party.customer_tax_rule.apply(tax, pattern)
|
|
if tax_ids:
|
|
taxes.extend(tax_ids)
|
|
continue
|
|
taxes.append(tax.id)
|
|
if party.customer_tax_rule:
|
|
tax_ids = party.customer_tax_rule.apply(None, pattern)
|
|
if tax_ids:
|
|
taxes.extend(tax_ids)
|
|
|
|
return taxes
|
|
|
|
|
|
class ShipmentOutReturnForceDraft(Wizard):
|
|
'ShipmentOut Force Draft'
|
|
__name__ = 'stock.shipment.out.return.force_draft'
|
|
start_state = 'force_draft'
|
|
force_draft = StateTransition()
|
|
|
|
def transition_force_draft(self):
|
|
id_ = Transaction().context['active_id']
|
|
|
|
ShipmentOutReturn = Pool().get('stock.shipment.out.return')
|
|
if id_:
|
|
shipments = ShipmentOutReturn.browse([id_])
|
|
ShipmentOutReturn.draft(shipments)
|
|
return 'end'
|