621 lines
22 KiB
Python
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,
|
|
}])
|