trytonpsk-rental/service.py

621 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 __future__ import with_statement
from datetime import datetime, date
from trytond.model import Workflow, ModelView, ModelSQL, fields
from trytond.report import Report
from trytond.pyson import Eval, If, In, Get, Bool
from trytond.transaction import Transaction
from trytond.pool import Pool
from trytond.wizard import Wizard, StateTransition
# from trytond.i18n import gettext
from trytond.exceptions import UserError
STATES = {
'readonly': Eval('state') != 'draft',
}
class DepositLine(Workflow, ModelSQL, ModelView):
'Deposit Line'
__name__ = 'rental.service.deposit_line'
_rec_name = 'issue'
rental_service = fields.Many2One('rental.service', 'Rental Service')
date = fields.Date('Start Date', required=True)
issue = fields.Char('Issue')
amount = fields.Numeric('Amount', required=True, digits=(16, 2))
class Service(Workflow, ModelSQL, ModelView):
'Rental Service'
__name__ = 'rental.service'
_rec_name = 'party'
number = fields.Char('Number', readonly=True, )
company = fields.Many2One('company.company', 'Company', required=True,
states=STATES, domain=[
('id', If(In('company',
Eval('context', {})), '=', '!='), Get(Eval('context', {}),
'company', 0))
])
booking = fields.Many2One('rental.booking', 'Booking', states=STATES)
product = fields.Many2One('product.product', 'Product', domain=[
('template.type', '=', 'assets')
], states={
'readonly': Eval('state').in_(['done', 'cancelled']),
})
equipment = fields.Many2One('maintenance.equipment', 'Equipment', states={
'readonly': Eval('state').in_(['done', 'cancelled']),
})
service_product = fields.Many2One('product.product', 'Service',
domain=[
('type', '=', 'service'),
('template.salable', '=', True),
], states=STATES)
party = fields.Many2One('party.party', 'Customer', required=True,
states=STATES)
deposit_paid = fields.Boolean('Deposit Paid', states=STATES)
unit_price = fields.Numeric('Unit Price', digits=(16, 2), states=STATES)
service_date = fields.Date('Service Date', states=STATES)
start_date = fields.Date('Start Date', required=True,
states={'readonly': Eval('state') == 'done'})
start_time = fields.Time('Start Time', required=False,
states={'readonly': Eval('state') == 'done'})
end_date = fields.Date('End Date', states={
'readonly': Eval('state') == 'done'
})
end_time = fields.Time('End Time', states={
'readonly': Eval('state') == 'done'
})
sale = fields.Many2One('sale.sale', 'Sale Order', states=STATES)
currency = fields.Many2One('currency.currency', 'Currency', required=True,
states={
'readonly': (Eval('state') != 'draft') | Eval('currency', 0),
}, depends=['state'])
balance = fields.Function(fields.Numeric('Balance', digits=(16, 2),
readonly=True), 'get_balance')
comment = fields.Text('Comment', states=STATES)
sales = fields.Many2Many('rental.service-sale.sale', 'rental', 'sale',
'Sales', readonly=True)
extra_charges = fields.Numeric('Extra Charges', digits=(16, 2))
payments = fields.Many2Many('rental.service-account.voucher', 'rental',
'voucher', 'Voucher', states=STATES, domain=[
('party', '=', Eval('party')),
('voucher_type', '=', 'receipt'),
('state', '=', 'posted'),
], depends=['party'])
state = fields.Selection([
('draft', 'Draft'),
('confirmed', 'Confirmed'),
('renewed', 'Renewed'),
('done', 'Done'),
('cancelled', 'Cancelled'),
], 'State', readonly=True, required=True)
state_string = state.translated('state')
bank_bsb = fields.Char('BSB', states={
'readonly': Eval('state') == 'done',
})
bank_account_number = fields.Char('Bank Account Number', states={
'readonly': Eval('state') == 'done',
})
days_expiration = fields.Function(fields.Integer('Days To Expiration',
depends=['hiring_time', 'start_date']), 'get_days_expiration',
searcher='search_time_expiration')
expiration_days_cache = fields.Integer('Expiration Days Cache')
hiring_time = fields.Integer('Hiring Time', states={
'readonly': True
})
hiring_time_uom = fields.Selection([
('hour', 'Hour'),
('day', 'Day'),
('week', 'Week'),
('month', 'Month'),
], 'Hiring Time UoM', states={
'required': Eval('state') == 'confirmed'
})
deposit_lines = fields.One2Many('rental.service.deposit_line',
'rental_service', 'Deposit Lines', states={
'readonly': Eval('state').in_(['done', 'cancelled'])
})
check_list = fields.One2Many('rental.service.product_check_list',
'rental_service', 'Product Check List', depends=['equipment'])
early_return = fields.Boolean('Early Return')
early_return_fee = fields.Numeric('Early Return Fee', digits=(16, 2),
states={
'invisible': ~Eval('early_return')
})
date_return = fields.DateTime('Date Return', states={
'readonly': Eval('state') == 'done',
})
odometer_start = fields.Char('Start Odometer', states=STATES)
odometer_end = fields.Char('End Odometer', states={
'readonly': Eval('state') == 'done',
})
cover_prices = fields.Boolean('Cover Prices')
customer_contract_agree = fields.Boolean('Customer Contract Agree')
photo_link_agree = fields.Char('Photo Link Contract Agree')
photo_link_signature = fields.Char('Photo Link Signature')
photo_link_party_id = fields.Char('Photo Link Party ID')
photo_return_device = fields.Char('Photo Return Device')
id_number = fields.Function(fields.Char('ID Number'), 'get_party_info')
type_document = fields.Function(fields.Char('Type Document'), 'get_party_info')
mobile = fields.Function(fields.Char('Mobile'), 'get_party_info')
subdivision = fields.Function(fields.Char('Subdivision'), 'get_party_info')
suburb = fields.Function(fields.Char('Suburb'), 'get_suburb')
post_code = fields.Function(fields.Char('Post Code'), 'get_party_info')
address = fields.Function(fields.Char('Address'), 'get_party_info')
email = fields.Function(fields.Char('Email'), 'get_party_info')
pickup_user = fields.Many2One('res.user', 'Pickup User')
deposit_state = fields.Selection([
('', ''),
('returned', 'Returned'),
('partial_paid', 'Partial Paid'),
('kept', 'Kept'),
], 'Payment State')
deposit_amount = fields.Numeric('Deposit Amount', digits=(16, 2))
deposit_kept = fields.Function(fields.Numeric('Deposit Kept Amount',
digits=(16, 2)), 'get_deposit_kept')
deposit_balance = fields.Function(fields.Numeric('Deposit Balance',
digits=(16, 2)), 'get_deposit_balance')
sended_expired_notificacion = fields.Boolean('Sended Expired Notification')
date_expired_notificacion = fields.Date('Date Expired Notification', readonly=True)
sended_deposit_notificacion = fields.Boolean('Sended Deposit Notification')
date_deposit_notificacion = fields.Date('Date Deposit Notification', readonly=True)
issues_presented = fields.Char('Issues Pressented')
part_missing = fields.Char('Part Missing/Broken')
@classmethod
def __setup__(cls):
super(Service, cls).__setup__()
cls._order = [
('service_date', 'DESC'),
('number', 'DESC'),
]
cls._transitions |= set((
('draft', 'cancel'),
('draft', 'confirmed'),
('confirmed', 'done'),
('confirmed', 'renewed'),
('renewed', 'done'),
('done', 'renewed'),
))
cls._buttons.update({
'draft': {
'invisible': Eval('state') != 'confirmed',
},
'cancel': {
'invisible': Eval('state') != 'draft',
},
'done': {
'invisible': ~Eval('state').in_(['confirmed', 'renewed']),
},
'confirm': {
'invisible': Eval('state') != 'draft',
},
'renew': {
'invisible': ~Eval('state').in_(['confirmed', 'done']),
},
'pay_advance': {
'invisible': Eval('state').in_(['done']),
},
'notification_expiration': {
'invisible': Bool(Eval('sended_expired_notificacion')),
},
'notification_deposit': {
'invisible': Bool(Eval('sended_deposit_notificacion')),
},
})
def get_rec_name(self, name):
return self.party.rec_name
@classmethod
def search_rec_name(cls, name, clause):
if clause[1].startswith('!') or clause[1].startswith('not '):
bool_op = 'AND'
else:
bool_op = 'OR'
return [bool_op, ('number',) + tuple(clause[1:])]
@classmethod
def copy(cls, records, default=None):
if default is None:
default = {}
default = default.copy()
default['number'] = None
default['state'] = 'draft'
new_records = []
for record in records:
new_record, = super(Service, cls).copy(
[record], default=default
)
new_records.append(new_record)
return new_records
@staticmethod
def default_hiring_time_uom():
config = Pool().get('rental.configuration')(1)
return config.uom_hiring_time
@staticmethod
def default_company():
return Transaction().context.get('company') or None
@staticmethod
def default_balance():
return 0
@staticmethod
def default_state():
return 'draft'
def _send(self, template, to_email, attach_=True):
pool = Pool()
config = pool.get('rental.configuration')(1)
Template = pool.get('email.template')
Template.send(getattr(config, template), self, to_email, attach=attach_)
def send_rental_emails(self):
self._send('email_rental_customer', self.party.email)
self._send('email_rental_company', self.company.party.email)
def send_return_emails(self):
self._send('email_rental_return_customer', self.party.email)
self._send('email_rental_return_company', self.company.party.email)
def send_expire_emails(self):
self._send('email_rental_expire_customer', self.party.email)
self._send('email_rental_expire_company', self.company.party.email)
def send_deposit_emails(self):
self._send('email_rental_return_deposit', self.party.email, False)
@staticmethod
def default_currency():
Company = Pool().get('company.company')
company = Transaction().context.get('company')
if company:
return Company(company).currency.id
@classmethod
@ModelView.button
@Workflow.transition('draft')
def draft(cls, records):
pass
@classmethod
@ModelView.button
@Workflow.transition('cancelled')
def cancel(cls, records):
pass
@classmethod
@ModelView.button
@Workflow.transition('renewed')
def renew(cls, records):
for rec in records:
rec.equipment.write([rec.equipment], {'status': 'not_available'})
rec.send_rental_emails()
@classmethod
@ModelView.button
@Workflow.transition('confirmed')
def confirm(cls, records):
cls.set_number(records)
for rec in records:
rec.send_rental_emails()
rec.equipment.write([rec.equipment], {'status': 'not_available'})
res = {
'status': 'success',
'msg': 'Agreement confirmed, please check your mail for more details!'
}
return res
@classmethod
@ModelView.button
@Workflow.transition('done')
def done(cls, records):
for rec in records:
rec.send_return_emails()
rec.equipment.write([rec.equipment], {'status': 'available'})
@classmethod
@ModelView.button_action('rental.wizard_service_advance_voucher')
def pay_advance(cls, bookings):
pass
@classmethod
@ModelView.button
def notification_expiration(cls, records):
for rec in records:
rec.send_expire_emails()
rec.sended_expired_notificacion = True
rec.date_expired_notificacion = date.today()
rec.save()
@classmethod
@ModelView.button
def notification_deposit(cls, records):
for rec in records:
rec.send_deposit_emails()
rec.sended_deposit_notificacion = True
rec.date_deposit_notificacion = date.today()
rec.save()
@classmethod
def validate(cls, records):
pass
def get_party_info(self, name=None):
if not self.party:
return
if name == 'mobile':
return self.party.mobile
if name == 'email':
return self.party.email
if name == 'address':
return self.party.addresses[0].street if self.party.addresses else None
if name == 'post_code':
return self.party.addresses[0].postal_code if self.party.addresses else None
if name == 'type_document':
return self.party.type_document
if name == 'id_number':
if hasattr(self.party, 'id_number'):
return self.party.id_number
if name == 'subdivision':
if self.party.addresses:
address = self.party.addresses[0]
if address.subdivision:
return address.subdivision.name
def get_suburb(self, name=None):
if self.booking:
if self.booking.suburb:
return self.booking.suburb
return ''
def create_sale(self):
pool = Pool()
Date = pool.get('ir.date')
Sale = pool.get('sale.sale')
SaleLine = pool.get('sale.line')
Party = pool.get('party.party')
today = Date.today()
sale, = Sale.create([{
'company': self.company.id,
'party': self.party.id,
'price_list': None,
'sale_date': today,
'state': 'draft',
'currency': self.currency.id,
'invoice_address': Party.address_get(self.party, type='invoice'),
'shipment_address': Party.address_get(self.party, type='delivery'),
'reference': 'No.' + self.number
}])
sale.save()
new_line = {
'sale': sale.id,
'type': 'line',
'unit': self.service_product.template.default_uom.id,
'quantity': 1,
'unit_price': self.service_product.template.list_price,
'product': self.service_product.id,
'description': self.service_product.rec_name,
}
SaleLine.create([new_line])
@classmethod
def write(cls, records, values):
format = '%Y-%m-%d'
end_date = values.get('end_date')
if end_date:
for rec in records:
if isinstance(end_date, str):
rec.end_date = datetime.strptime(end_date, format).date()
values['hiring_time'] = rec.on_change_with_hiring_time()
super(Service, cls).write(records, values)
@classmethod
def set_number(cls, records):
'''
Fill the number field with the service sequence
'''
pool = Pool()
Sequence = pool.get('ir.sequence')
Config = pool.get('rental.configuration')
config = Config(1)
for rec in records:
if rec.number:
continue
if not config.rental_service_sequence:
continue
number = config.rental_service_sequence.get()
cls.write([rec], {'number': number})
@fields.depends('unit_price', 'service_product', 'hiring_time')
def on_change_service_product(self, name=None):
if self.service_product and self.hiring_time:
self.unit_price = self.service_product.list_price * self.hiring_time
else:
self.unit_price = None
@fields.depends('start_date', 'end_date')
def on_change_with_hiring_time(self, name=None):
if self.start_date and self.end_date:
return (self.end_date - self.start_date).days
@fields.depends('date_return', 'end_date', 'early_return')
def on_change_with_early_return(self, name=None):
if self.date_return and self.end_date:
days = (self.end_date - self.date_return).days
if int(days) > 0:
return True
# @classmethod
# def create_check_list(cls, requests):
# pool = Pool()
# CheckListEquipment = pool.get('maintenance.check_list_equipment')
# CheckListRental = pool.get('rental.service.product_check_list')
# check_list_equipment = CheckListEquipment.search([
# ('equipment', '=', requests[0].equipment.id),
# ])
# for ch in check_list_equipment:
# check_list = {
# 'item': ch.item.id,
# 'state_delivery': ch.state_delivery,
# 'state_return': ch.state_return,
# 'rental_service': requests[0].id,
# }
# CheckListRental.create([check_list])
@fields.depends('payments', 'unit_price', 'early_return')
def get_balance(self, name=None):
Configuration = Pool().get('rental.configuration')
config = Configuration.search([])
amounts = []
amount_to_pay = 0
if self.unit_price:
amount_to_pay += self.unit_price
if self.payments:
for payment in self.payments:
amounts.append(payment.amount_to_pay)
amount_to_pay -= sum(amounts)
if self.early_return:
amount_to_pay += config[0].value_early_return
return amount_to_pay
def get_deposit_kept(self, name):
return sum([l.amount for l in self.deposit_lines])
def get_deposit_balance(self, name):
if self.deposit_amount and self.deposit_kept:
return (self.deposit_amount - self.deposit_kept)
def get_days_expiration(self, name):
if self.end_date:
days = (self.end_date - date.today()).days
return days
@classmethod
def search_time_expiration(cls, name, clause):
records = cls.search([
('state', '=', 'confirmed')
])
targets = []
try:
_lapse = int(clause[2])
except:
_lapse = 7 # days
for r in records:
if r.days_expiration and r.days_expiration <= _lapse:
targets.append(r.id)
return ('id', 'in', targets)
class RentalProductCheckList(ModelSQL, ModelView):
"Rental Product Check List"
__name__ = "rental.service.product_check_list"
rental_service = fields.Many2One('rental.service', 'Rental',
ondelete='CASCADE', required=True)
item = fields.Many2One('product.check_list_item', 'Item', required=True)
state_delivery = fields.Boolean('State Delivery')
state_return = fields.Boolean('State Return')
kind = fields.Selection([
('', ''),
('item', 'Item'),
('category', 'Category'),
], 'Kind')
class RentalServiceReport(Report):
__name__ = 'rental.service.report'
@classmethod
def get_context(cls, records, header, data):
report_context = super().get_context(records, header, data)
user = Pool().get('res.user')(Transaction().user)
report_context['company'] = user.company
report_context['user'] = user
dat = date.today()
report_context['today'] = dat
return report_context
class RentalEquipmentReturnReport(Report):
__name__ = 'rental.equipment_return.report'
@classmethod
def get_context(cls, records, header, data):
report_context = super().get_context(records, header, data)
user = Pool().get('res.user')(Transaction().user)
report_context['company'] = user.company
report_context['user'] = user
dat = date.today()
report_context['today'] = dat
return report_context
class CacheExpirationDays(Wizard):
'Service Force Draft'
__name__ = 'rental.service.cache_expiration_days'
start_state = 'cache'
cache = StateTransition()
def transition_cache(self):
Service = Pool().get('rental.service')
services = Service.search([
('state', '=', 'confirmed')
])
for s in services:
Service.write([s], {'expiration_days_cache': s.days_expiration})
return 'end'
class ServiceForceDraft(Wizard):
'Service Force Draft'
__name__ = 'rental.service.force_draft'
start_state = 'force_draft'
force_draft = StateTransition()
def transition_force_draft(self):
cursor = Transaction().connection.cursor()
ids = Transaction().context['active_ids']
if ids:
ids = str(ids[0])
query = "UPDATE rental_service SET state='draft' WHERE id=%s"
cursor.execute(query % ids)
return 'end'
class RentalSale(ModelSQL):
'Rental - Sale'
__name__ = 'rental.service-sale.sale'
_table = 'rental_sales_rel'
rental = fields.Many2One('rental.service', 'Rental', ondelete='CASCADE',
required=True)
sale = fields.Many2One('sale.sale', 'Sale', ondelete='RESTRICT',
required=True)
class RentalVoucher(ModelSQL):
'Rental - Voucher'
__name__ = 'rental.service-account.voucher'
_table = 'rental_vouchers_rel'
rental = fields.Many2One('rental.service', 'Rental', ondelete='CASCADE',
required=True)
voucher = fields.Many2One('account.voucher', 'Voucher',
domain=[('voucher_type', '=', 'receipt')],
ondelete='RESTRICT', required=True)
@classmethod
def set_voucher_origin(cls, voucher_id, rental_id):
cls.create([{
'voucher': voucher_id,
'rental': rental_id,
}])