trytonpsk-rental/stock.py

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'