Files
tradon/modules/sale_rental/sale.py
2026-03-14 09:42:12 +00:00

1792 lines
61 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# This file is part of Tryton. The COPYRIGHT file at the top level of
# this repository contains the full copyright notices and license terms.
import datetime as dt
from decimal import Decimal
from functools import partial
from trytond.i18n import gettext
from trytond.ir.attachment import AttachmentCopyMixin
from trytond.ir.note import NoteCopyMixin
from trytond.model import (
ChatMixin, Index, ModelSQL, ModelView, Unique, Workflow, dualmethod,
fields, sequence_ordered)
from trytond.model.exceptions import AccessError, ValidationError
from trytond.model.fields.date import FormatMixin
from trytond.modules.account.tax import TaxableMixin
from trytond.modules.company.model import (
employee_field, reset_employee, set_employee)
from trytond.modules.currency.fields import Monetary
from trytond.modules.product import price_digits, round_price
from trytond.pool import Pool, PoolMeta
from trytond.pyson import Date, DateTime, Eval, Id, If
from trytond.report import Report
from trytond.tools import timezone as tz
from trytond.transaction import Transaction
from trytond.wizard import Button, StateTransition, StateView, Wizard
_datetime_format = '%H:%M'
_time_min = dt.datetime.strptime(
dt.time.min.strftime(_datetime_format), _datetime_format).time()
_time_max = dt.datetime.strptime(
dt.time.max.strftime(_datetime_format), _datetime_format).time()
class DateOrDateTime(FormatMixin, fields.Date):
def __init__(self, *args, **kwargs):
self.format = kwargs.pop('format')
super().__init__(*args, **kwargs)
def definition(self, model, language):
definition = super().definition(model, language)
definition['type'] = 'datetime'
return definition
def to_datetime(value, company=None, time=None):
if isinstance(value, dt.date) and not isinstance(value, dt.datetime):
if time is None:
time = _time_min
if company and company.timezone:
timezone = tz.ZoneInfo(company.timezone)
else:
timezone = None
value = (dt.datetime.combine(value, time, timezone)
.astimezone(tz.UTC)
.replace(tzinfo=None))
return value
def to_date(value, company=None):
if isinstance(value, dt.datetime):
if company and company.timezone:
timezone = tz.ZoneInfo(company.timezone)
else:
timezone = None
value = (value
.replace(tzinfo=tz.UTC)
.astimezone(timezone)
.replace(tzinfo=None)
.date())
return value
class Configuration(metaclass=PoolMeta):
__name__ = 'sale.configuration'
rental_sequence = fields.MultiValue(fields.Many2One(
'ir.sequence', "Rental Sequence", required=True,
domain=[
('company', 'in',
[Eval('context', {}).get('company', -1), None]),
('sequence_type', '=',
Id('sale_rental', 'sequence_type_rental')),
]))
@classmethod
def multivalue_model(cls, field):
pool = Pool()
if field == 'rental_sequence':
return pool.get('sale.configuration.sequence')
return super().multivalue_model(field)
@classmethod
def default_rental_sequence(cls, **pattern):
return cls.multivalue_model(
'rental_sequence').default_rental_sequence()
class ConfigurationSequence(metaclass=PoolMeta):
__name__ = 'sale.configuration.sequence'
rental_sequence = fields.Many2One(
'ir.sequence', "Rental Sequence", required=True,
domain=[
('company', 'in', [Eval('company', -1), None]),
('sequence_type', '=',
Id('sale_rental', 'sequence_type_rental')),
])
@classmethod
def default_rental_sequence(cls):
pool = Pool()
ModelData = pool.get('ir.model.data')
try:
return ModelData.get_id(
'sale_rental', 'sequence_rental')
except KeyError:
return None
class Rental(
Workflow, ModelSQL, ModelView, TaxableMixin,
AttachmentCopyMixin, NoteCopyMixin, ChatMixin):
__name__ = 'sale.rental'
_rec_name = 'number'
company = fields.Many2One(
'company.company', "Company", required=True,
states={
'readonly': (Eval('state') != 'draft') | Eval('party', True),
},
help="Make the sale rental belong to the company.")
number = fields.Char(
"Number", readonly=True,
help="The main identification of the sale rental.")
reference = fields.Char(
"Reference",
help="The identification of an external origin.")
description = fields.Char(
"Description",
states={
'readonly': Eval('state') != 'draft',
})
party = fields.Many2One(
'party.party', "Party", required=True,
states={
'readonly': (
(Eval('state') != 'draft')
| (Eval('lines', [0]) & Eval('party'))),
},
context={
'company': Eval('company', -1),
},
depends=['company'],
help="The party who is renting.")
contact = fields.Many2One(
'party.contact_mechanism', "Contact",
context={
'company': Eval('company', -1),
},
search_context={
'related_party': Eval('party'),
},
depends={'company', 'party'})
invoice_party = fields.Many2One('party.party', "Invoice Party",
states={
'readonly': ((Eval('state') != 'draft')
| Eval('lines', [0])),
},
context={
'company': Eval('company', -1),
},
search_context={
'related_party': Eval('party'),
},
depends={'company', 'party'})
invoice_address = fields.Many2One(
'party.address', "Invoice Address",
domain=[
('party', '=', If(Eval('invoice_party'),
Eval('invoice_party', -1), Eval('party', -1))),
],
states={
'readonly': Eval('state') != 'draft',
'required': ~Eval('state').in_(['draft', 'cancelled']),
})
payment_term = fields.Many2One(
'account.invoice.payment_term', "Payment Term",
states={
'readonly': Eval('state') != 'draft',
})
warehouse = fields.Many2One(
'stock.location', "Warehouse",
domain=[
('type', '=', 'warehouse'),
],
states={
'readonly': Eval('state') != 'draft',
'required': ~Eval('state').in_(['draft', 'cancelled']),
})
currency = fields.Many2One(
'currency.currency', "Currency", required=True,
states={
'readonly': ((Eval('state') != 'draft')
| (Eval('lines', [0]) & Eval('currency', 0))),
})
start = fields.Function(
DateOrDateTime("Start", format=_datetime_format),
'on_change_with_start')
end = fields.Function(
DateOrDateTime("End", format=_datetime_format),
'on_change_with_end')
lines = fields.One2Many(
'sale.rental.line', 'rental', "Lines",
states={
'readonly': Eval('state') != 'draft',
},
depends=['start', 'end'])
outgoing_moves = fields.One2Many(
'stock.move', 'origin', "Outgoing Moves", readonly=True,
filter=[
('to_location.type', '=', 'rental'),
])
incoming_moves = fields.One2Many(
'stock.move', 'origin', "Incoming Moves", readonly=True,
filter=[
('from_location.type', '=', 'rental'),
])
has_returnable_lines = fields.Function(
fields.Boolean("Has Returnable Lines"),
'get_has_returnable_lines')
has_lines_to_invoice = fields.Function(
fields.Boolean("Has Lines to Invoice"),
'get_has_lines_to_invoice')
untaxed_amount = fields.Function(
Monetary("Untaxed", digits='currency', currency='currency'),
'get_amount')
untaxed_amount_cache = fields.Numeric(
"Untaxed Cache", digits='currency', readonly=True)
tax_amount = fields.Function(
Monetary("Tax", digits='currency', currency='currency'),
'get_amount')
tax_amount_cache = fields.Numeric(
"Tax Cache", digits='currency', readonly=True)
total_amount = fields.Function(
Monetary("Total", digits='currency', currency='currency'),
'get_amount')
total_amount_cache = fields.Numeric(
"Total Cache", digits='currency', readonly=True)
quoted_by = employee_field(
"Quoted By", states=['quotation', 'confirmed', 'done', 'cancelled'])
confirmed_by = employee_field(
"Confirmed By", states=['confirmed', 'done', 'cancelled'])
state = fields.Selection([
('draft', "Draft"),
('quotation', "Quotation"),
('confirmed', "Confirmed"),
('picked up', "Picked up"),
('done', "Done"),
('cancelled', "Cancelled"),
], "State", readonly=True, required=True, sort=False,
help="The current state of the sale rental.")
@classmethod
def __setup__(cls):
cls.number.search_unaccented = False
cls.reference.search_unaccented = False
super().__setup__()
t = cls.__table__()
cls._sql_indexes.update({
Index(t, (t.reference, Index.Similarity())),
Index(t, (t.party, Index.Equality())),
Index(
t,
(t.state, Index.Equality()),
where=t.state.in_([
'draft', 'quotation', 'confirmed', 'picked up'])),
})
cls._transitions |= {
('draft', 'quotation'),
('draft', 'cancelled'),
('quotation', 'confirmed'),
('quotation', 'draft'),
('quotation', 'cancelled'),
('confirmed', 'picked up'),
('confirmed', 'draft'),
('picked up', 'done'),
('picked up', 'draft'),
('cancelled', 'draft'),
}
cls._buttons.update({
'cancel': {
'invisible': ~Eval('state').in_(['draft', 'quotation']),
'icon': 'tryton-cancel',
'depends': ['state'],
},
'draft': {
'invisible': ~Eval('state').in_(
['quotation', 'confirmed', 'picked up', 'cancelled']),
'icon': If(Eval('state') == 'cancelled',
'tryton-undo', 'tryton-back'),
'depends': ['state'],
},
'quote': {
'invisible': Eval('state') != 'draft',
'readonly': ~Eval('lines', []),
'icon': 'tryton-forward',
'depends': ['state'],
},
'confirm': {
'invisible': Eval('state') != 'quotation',
'icon': 'tryton-forward',
'depends': ['state'],
},
'pickup': {
'invisible': Eval('state') != 'confirmed',
'icon': 'tryton-shipment-out',
'depends': ['state'],
},
'return_': {
'invisible': (
Eval('state').in_(['draft', 'quote'])
| ~Eval('has_returnable_lines')),
'icon': 'tryton-shipment-in',
'depends': ['state', 'has_returnable_lines'],
},
'invoice': {
'invisible': (
Eval('state').in_(['draft', 'quote'])
| ~Eval('has_lines_to_invoice')),
'icon': 'tryton-invoice',
'depends': ['has_lines_to_invoice'],
},
})
cls._states_cached = {'confirmed', 'picked up', 'done', 'cancelled'}
@classmethod
def default_company(cls):
return Transaction().context.get('company')
@classmethod
def default_warehouse(cls):
Location = Pool().get('stock.location')
return Location.get_default_warehouse()
@classmethod
def default_currency(cls, **pattern):
pool = Pool()
Company = pool.get('company.company')
company = pattern.get('company')
if not company:
company = cls.default_company()
if company:
return Company(company).currency.id
@classmethod
def default_state(cls):
return 'draft'
@fields.depends('company', 'party', 'invoice_party', 'currency', 'lines')
def on_change_party(self):
if not self.invoice_party:
self.invoice_address = None
if not self.lines:
self.currency = self.default_currency(
company=self.company.id if self.company else None)
if self.party:
if not self.invoice_party:
self.invoice_address = self.party.address_get(type='invoice')
self.payment_term = self.party.customer_payment_term
if not self.lines and self.party.customer_currency:
self.currency = self.party.customer_currency
@fields.depends('party', 'invoice_party')
def on_change_invoice_party(self):
if self.invoice_party:
self.invoice_address = self.invoice_party.address_get(
type='invoice')
elif self.party:
self.invoice_address = self.party.address_get(type='invoice')
@classmethod
def default_start(cls):
now = dt.datetime.now().replace(second=0, microsecond=0)
now += dt.timedelta(minutes=30 - now.minute % 30)
return now
@fields.depends('company', 'lines')
def on_change_with_start(self, name=None):
return min(map(partial(to_datetime, company=self.company), filter(
None,
(getattr(l, 'start', None) for l in (self.lines or [])))),
default=self.default_start())
@fields.depends('company', 'lines')
def on_change_with_end(self, name=None):
return max(map(partial(
to_datetime, company=self.company, time=_time_max),
filter(
None,
(getattr(l, 'end', None) for l in (self.lines or [])))),
default=None)
@fields.depends('lines', 'currency', methods=['get_tax_amount'])
def on_change_lines(self):
self.untaxed_amount = Decimal(0)
self.tax_amount = Decimal(0)
self.total_amount = Decimal(0)
for line in self.lines or []:
self.untaxed_amount += getattr(line, 'amount', None) or 0
self.tax_amount = self.get_tax_amount()
if self.currency:
self.untaxed_amount = self.currency.round(self.untaxed_amount)
self.tax_amount = self.currency.round(self.tax_amount)
self.total_amount = self.untaxed_amount + self.tax_amount
if self.currency:
self.total_amount = self.currency.round(self.total_amount)
def get_has_returnable_lines(self, name):
return any(l.rental_state == 'picked up' for l in self.lines)
def get_has_lines_to_invoice(self, name):
return any(l.to_invoice for l in self.lines)
@fields.depends(methods=['_get_taxes'])
def get_tax_amount(self):
return sum(
(t.amount for t in self._get_taxes().values()), Decimal(0))
@property
def taxable_lines(self):
taxable_lines = []
for line in self.lines:
taxable_lines.extend(line.taxable_lines)
return taxable_lines
@fields.depends('party', 'company')
def _get_tax_context(self):
context = {}
if self.party and self.party.lang:
context['language'] = self.party.lang.code
if self.company:
context['company'] = self.company.id
return context
@classmethod
def get_amount(cls, rentals, names):
untaxed_amount = {}
tax_amount = {}
total_amount = {}
if {'tax_amount', 'total_amount'} & set(names):
compute_taxes = True
else:
compute_taxes = False
# Sort cached first and re-instanciate to optimize cache management
rentals = sorted(
rentals, key=lambda s: s.state in cls._states_cached, reverse=True)
for rental in cls.browse(rentals):
if (rental.state in cls._states_cached
and rental.untaxed_amount_cache is not None
and rental.tax_amount_cache is not None
and rental.total_amount_cache is not None):
untaxed_amount[rental.id] = rental.untaxed_amount_cache
if compute_taxes:
tax_amount[rental.id] = rental.tax_amount_cache
total_amount[rental.id] = rental.total_amount_cache
else:
untaxed_amount[rental.id] = sum(
(line.amount for line in rental.lines), Decimal(0))
if compute_taxes:
tax_amount[rental.id] = rental.get_tax_amount()
total_amount[rental.id] = (
untaxed_amount[rental.id] + tax_amount[rental.id])
amounts = {}
if 'untaxed_amount' in names:
amounts['untaxed_amount'] = untaxed_amount
if 'tax_amount' in names:
amounts['tax_amount'] = tax_amount
if 'total_amount' in names:
amounts['total_amount'] = total_amount
return amounts
@classmethod
def set_number(cls, rentals):
pool = Pool()
Config = pool.get('sale.configuration')
config = Config(1)
for rental in rentals:
if rental.number:
continue
rental.number = config.get_multivalue(
'rental_sequence',
company=rental.company.id).get()
cls.save(rentals)
@classmethod
def set_moves(cls, rentals):
pool = Pool()
Line = pool.get('sale.rental.line')
Move = pool.get('stock.move')
lines, moves = [], set()
for rental in rentals:
for line in rental.lines:
line.outgoing_moves = line.get_moves('out')
moves.update(line.outgoing_moves)
line.incoming_moves = line.get_moves('in')
moves.update(line.incoming_moves)
lines.append(line)
Move.save(moves)
Line.save(lines)
@classmethod
def delete_moves(cls, rentals, all_=False):
pool = Pool()
Line = pool.get('sale.rental.line')
Move = pool.get('stock.move')
lines, moves = [], set()
for rental in rentals:
for line in rental.lines:
moves.update(line.outgoing_moves)
line.outgoing_moves = []
moves.update(line.incoming_moves)
line.incoming_moves = []
lines.append(line)
Line.save(lines)
moves = Move.browse(moves)
Move.draft(moves)
Move.delete([
m for m in moves if all_ or m.state in {'draft', 'cancelled'}])
@classmethod
def store_cache(cls, rentals):
for rental in rentals:
rental.untaxed_amount_cache = rental.untaxed_amount
rental.tax_amount_cache = rental.tax_amount
rental.total_amount_cache = rental.total_amount
cls.save(rentals)
def create_invoice(self):
invoice_lines = []
for line in self.lines:
if line.to_invoice:
invoice_lines.extend(line.get_invoice_lines())
if not invoice_lines:
return
invoice = self._get_invoice()
if getattr(invoice, 'lines', None):
invoice_lines = list(invoice.lines) + invoice_lines
invoice.lines = invoice_lines
return invoice
def _get_invoice(self):
pool = Pool()
Invoice = pool.get('account.invoice')
party = self.invoice_party or self.party
invoice = Invoice(
company=self.company,
type='out',
party=party,
invoice_address=self.invoice_address,
currency=self.currency,
account=party.account_receivable_used,
)
invoice.set_journal()
invoice.payment_term = self.payment_term
return invoice
def get_rec_name(self, name):
items = []
if self.number:
items.append(self.number)
if self.reference:
items.append('[%s]' % self.reference)
if not items:
items.append('(%s)' % self.id)
return ' '.join(items)
@classmethod
def search_rec_name(cls, name, clause):
_, operator, value = clause
if operator.startswith('!') or operator.startswith('not '):
bool_op = 'AND'
else:
bool_op = 'OR'
domain = [bool_op,
('number', operator, value),
('reference', operator, value),
]
return domain
def chat_language(self, audience='internal'):
language = super().chat_language(audience=audience)
if audience == 'public':
language = self.party.lang.code if self.party.lang else None
return language
@classmethod
def copy(cls, rentals, default=None):
default = default.copy() if default is not None else {}
default.setdefault('number', None)
default.setdefault('quoted_by')
default.setdefault('confirmed_by')
default.setdefault('lines.outgoing_moves', None)
default.setdefault('lines.incoming_moves', None)
return super().copy(rentals, default=default)
@classmethod
def view_attributes(cls):
return super().view_attributes() + [
('/tree', 'visual',
If(Eval('state') == 'cancelled', 'muted', '')),
]
@classmethod
@ModelView.button
@Workflow.transition('cancelled')
def cancel(cls, rentals):
cls.delete_moves(rentals, all_=True)
@classmethod
@ModelView.button
@Workflow.transition('draft')
@reset_employee('quoted_by', 'confirmed_by')
def draft(cls, rentals):
pool = Pool()
Line = pool.get('sale.rental.line')
# Set actual period as planned period
lines = []
for rental in rentals:
for line in rental.lines:
if line.per_day:
if line.actual_start:
line.planned_start_day = to_date(
line.actual_start, line.company)
if line.actual_end:
line.planned_end_day = to_date(
line.actual_end, line.company)
if line.planned_end_day < line.planned_start_day:
line.planned_end_day = line.planned_start_day
else:
if line.actual_start:
line.planned_start = line.actual_start
if line.actual_end:
line.planned_end = line.actual_end
if line.planned_end < line.planned_start:
line.planned_end = line.planned_start
lines.append(line)
Line.save(lines)
cls.delete_moves(rentals)
@classmethod
@ModelView.button
@Workflow.transition('quotation')
@set_employee('quoted_by')
def quote(cls, rentals):
cls.set_number(rentals)
cls.set_moves(rentals)
@classmethod
@ModelView.button
@Workflow.transition('confirmed')
@set_employee('confirmed_by')
def confirm(cls, rentals):
cls.store_cache(rentals)
cls.write(rentals, {'state': 'confirmed'})
cls.try_picked_up(rentals)
@classmethod
@ModelView.button_action('sale_rental.wizard_sale_rental_pickup')
def pickup(cls, rentals):
pass
@dualmethod
def try_picked_up(cls, rentals):
to_picked_up = []
for rental in rentals:
if all(
l.rental_state in {'picked up', 'done'}
for l in rental.lines):
to_picked_up.append(rental)
if to_picked_up:
cls.picked_up(to_picked_up)
@classmethod
@Workflow.transition('picked up')
def picked_up(cls, rentals):
cls.write(rentals, {'state': 'picked up'})
cls.try_done(rentals)
@classmethod
@ModelView.button_action('sale_rental.wizard_sale_rental_return')
def return_(cls, rentals):
pass
@classmethod
@ModelView.button
def invoice(cls, rentals):
pool = Pool()
Invoice = pool.get('account.invoice')
invoices = {}
for rental in rentals:
invoice = rental.create_invoice()
if invoice:
invoices[rental] = invoice
Invoice.save(invoices.values())
Invoice.update_taxes(invoices.values())
for sale, invoice in invoices.items():
sale.copy_resources_to(invoice)
@dualmethod
def try_done(cls, rentals):
to_do = []
for rental in rentals:
if all(l.rental_state == 'done' for l in rental.lines):
to_do.append(rental)
if to_do:
cls.do(to_do)
@classmethod
@Workflow.transition('done')
def do(cls, rentals):
cls.invoice(rentals)
class RentalLine(sequence_ordered(), ModelSQL, ModelView, TaxableMixin):
__name__ = 'sale.rental.line'
rental = fields.Many2One(
'sale.rental', "Rental", required=True, ondelete='CASCADE',
states={
'readonly': (
(Eval('rental_state') != 'draft')
& Eval('rental')),
},
help="Add the line below the rental.")
product = fields.Many2One(
'product.product', "Product", required=True, ondelete='RESTRICT',
domain=[
('type', 'in', ['assets', 'service']),
('rentable', '=', True),
],
states={
'readonly': Eval('rental_state') != 'draft',
},
context={
'company': Eval('company', -1),
},
search_context={
'locations': If(Eval('_parent_rental', {}).get('warehouse'),
[Eval('_parent_rental', {}).get('warehouse', -1)], []),
'stock_date_end': Date(start=Eval('start', DateTime())),
'stock_skip_warehouse': True,
'rental_date': Date(start=Eval('start', DateTime())),
'currency': Eval('_parent_rental', {}).get('currency', -1),
'customer': Eval('_parent_rental', {}).get('party', -1),
'uom': Eval('unit', -1),
'taxes': Eval('taxes', []),
'quantity': Eval('quantity', 0),
'duration': Eval('duration', None),
},
depends=['company'])
per_day = fields.Boolean(
"Per Day",
states={
'readonly': Eval('rental_state') != 'draft',
})
quantity = fields.Float(
"Quantity", digits='unit', required=True,
domain=[
If(Eval('quantity'),
('quantity', '>=', 0),
()),
],
states={
'readonly': Eval('rental_state') != 'draft',
})
unit = fields.Many2One(
'product.uom', "Unit", required=True,
states={
'readonly': Eval('rental_state') != 'draft',
},
domain=[
If(Eval('product_uom_category'),
('category', '=', Eval('product_uom_category', -1)),
('category', '=', -1)),
])
planned_start_day = fields.Date(
"Planned Start",
domain=[
If(Eval('planned_start_day') & Eval('planned_end_day'),
('planned_start_day', '<=', Eval('planned_end_day', None)),
()),
If(~Eval('per_day'),
('planned_start_day', '=', None),
()),
],
states={
'required': Eval('per_day', False),
'invisible': ~Eval('per_day'),
'readonly': Eval('rental_state') != 'draft',
})
planned_end_day = fields.Date(
"Planned End",
domain=[
If(Eval('planned_start_day') & Eval('planned_end_day'),
('planned_end_day', '>=', Eval('planned_start_day', None)),
()),
If(~Eval('per_day'),
('planned_end_day', '=', None),
()),
],
states={
'required': Eval('per_day', False),
'invisible': ~Eval('per_day'),
'readonly': Eval('rental_state') != 'draft',
})
planned_start = fields.DateTime(
"Planned Start", format=_datetime_format,
domain=[
If(Eval('planned_start') & Eval('planned_end'),
('planned_start', '<=', Eval('planned_end')),
()),
If(Eval('per_day', False),
('planned_start', '=', None),
()),
],
states={
'required': ~Eval('per_day'),
'invisible': Eval('per_day', False),
'readonly': Eval('rental_state') != 'draft',
})
planned_end = fields.DateTime(
"Planned End", format=_datetime_format,
domain=[
If(Eval('planned_start') & Eval('planned_end'),
('planned_end', '>=', Eval('planned_start')),
()),
If(Eval('per_day', False),
('planned_end', '=', None),
()),
],
states={
'required': ~Eval('per_day'),
'invisible': Eval('per_day', False),
'readonly': Eval('rental_state') != 'draft',
})
planned_duration = fields.Function(
fields.TimeDelta(
"Planned Duration",
states={
'invisible': ~Eval('planned_duration'),
}),
'on_change_with_planned_duration')
actual_start = fields.DateTime(
"Actual Start", format=_datetime_format, readonly=True,
domain=[
If(Eval('actual_start') & Eval('actual_end'),
('actual_start', '<=', Eval('actual_end')),
()),
],
states={
'invisible': ~Eval('actual_start'),
'required': Eval('rental_state').in_(['picked up', 'done']),
})
actual_end = fields.DateTime(
"Actual End", format=_datetime_format, readonly=True,
domain=[
If(Eval('actual_start') & Eval('actual_end'),
('actual_end', '>=', Eval('actual_start')),
()),
],
states={
'invisible': ~Eval('actual_end'),
'required': Eval('rental_state') == 'done',
})
start = fields.Function(
DateOrDateTime("Start", format=_datetime_format),
'on_change_with_start')
end = fields.Function(
DateOrDateTime("End", format=_datetime_format),
'on_change_with_end')
duration = fields.Function(
fields.TimeDelta(
"Duration",
states={
'invisible': ~Eval('duration'),
}),
'on_change_with_duration')
unit_price = Monetary(
"Unit Price", currency='currency', digits=price_digits, required=True,
states={
'readonly': Eval('rental_state') != 'draft',
})
unit_price_unit = fields.Many2One(
'product.uom', "Unit Price Unit", required=True,
domain=[
('category', '=', Id('product', 'uom_cat_time')),
],
states={
'readonly': Eval('rental_state') != 'draft',
})
taxes = fields.Many2Many(
'sale.rental.line-account.tax', 'line', 'tax', "Taxes",
order=[('tax.sequence', 'ASC'), ('tax.id', 'ASC')],
domain=[
('parent', '=', None),
['OR',
('group', '=', None),
('group.kind', 'in', ['sale', 'both'])],
('company', '=', Eval('company', -1)),
],
states={
'readonly': Eval('rental_state') != 'draft',
})
planned_amount = fields.Function(
Monetary(
"Planned Amount", digits='currency', currency='currency',
states={
'invisible': ~Eval('planned_duration'),
}),
'on_change_with_planned_amount')
amount = fields.Function(
Monetary(
"Amount", digits='currency', currency='currency',
states={
'invisible': ~Eval('duration'),
}),
'on_change_with_amount')
outgoing_moves = fields.Many2Many(
'sale.rental.line-outgoing-stock.move', 'line', 'move',
"Outgoing Moves",
states={
'readonly': True,
})
incoming_moves = fields.Many2Many(
'sale.rental.line-incoming-stock.move', 'line', 'move',
"Incoming Moves",
states={
'readonly': True,
})
invoice_lines = fields.One2Many(
'account.invoice.line', 'origin', "Invoice Lines", readonly=True)
rental_state = fields.Function(
fields.Selection('get_rental_states', "Rental State"),
'on_change_with_rental_state')
company = fields.Function(
fields.Many2One('company.company', "Company"),
'on_change_with_company')
currency = fields.Function(
fields.Many2One('currency.currency', "Currency"),
'on_change_with_currency')
product_uom_category = fields.Function(
fields.Many2One('product.uom.category', "Product UoM Category"),
'on_change_with_product_uom_category')
@classmethod
def __setup__(cls):
super().__setup__()
cls.__access__.add('rental')
@fields.depends(
'rental', 'per_day', '_parent_rental.company',
'_parent_rental.start', '_parent_rental.end',
'planned_start_day', 'planned_end_day',
'planned_start', 'planned_end',
methods=['on_change_with_duration'])
def _set_planned(self):
if self.rental:
if self.per_day:
if not self.planned_start_day:
self.planned_start_day = to_date(
self.rental.start, self.rental.company)
if not self.planned_end_day:
self.planned_end_day = to_date(
self.rental.end, self.rental.company)
self.planned_start = self.planned_end = None
else:
self.planned_start_day = self.planned_end_day = None
if not self.planned_start:
self.planned_start = to_datetime(
self.rental.start, self.rental.company)
if not self.planned_end:
self.planned_end = to_datetime(
self.rental.end, self.rental.company, _time_max)
self.duration = self.on_change_with_duration()
@fields.depends(
'product', 'unit',
methods=[
'compute_taxes', 'compute_unit_price', 'on_change_with_amount',
'_set_planned'])
def on_change_product(self):
if not self.product:
return
self.per_day = self.product.rental_per_day
self._set_planned()
self.taxes = self.compute_taxes()
if (not self.unit
or self.unit.category != self.product.default_uom.category):
self.unit = self.product.default_uom
self.unit_price_unit = self.product.rental_unit
self.unit_price = self.compute_unit_price()
self.amount = self.on_change_with_amount()
@fields.depends(
'product',
'rental', '_parent_rental.invoice_party', '_parent_rental.party',
methods=['_get_tax_rule_pattern'])
def compute_taxes(self):
party = None
if self.rental:
party = self.rental.invoice_party or self.rental.party
taxes = set()
pattern = self._get_tax_rule_pattern()
for tax in self.product.customer_rental_taxes_used:
if party and party.customer_tax_rule:
tax_ids = party.customer_tax_rule.apply(tax, pattern)
if tax_ids:
taxes.update(tax_ids)
continue
taxes.add(tax.id)
if party and party.customer_tax_rule:
tax_ids = party.customer_tax_rule.apply(None, pattern)
if tax_ids:
taxes.update(tax_ids)
return list(taxes)
def _get_tax_rule_pattern(self):
return {}
@property
def taxable_lines(self):
# In case we're called from an on_change
# we have to use some sensible defaults
return [(
getattr(self, 'taxes', None) or [],
getattr(self, 'unit_price', None) or Decimal(0),
(getattr(self, 'quantity', None) or 0)
* (getattr(self, 'duration_unit', None) or 0),
None,
)]
def _get_tax_context(self):
return self.rental._get_tax_context()
@fields.depends(
'product', 'quantity', 'unit_price_unit',
methods=['on_change_with_duration', '_get_context_rental_price'])
def compute_unit_price(self):
pool = Pool()
Product = pool.get('product.product')
UoM = pool.get('product.uom')
if not self.product:
return
duration = self.on_change_with_duration()
with Transaction().set_context(self._get_context_rental_price()):
unit_price = Product.get_rental_price(
[self.product],
quantity=self.quantity or 0,
duration=duration or dt.timedelta())[self.product.id]
if unit_price is not None:
if self.unit_price_unit:
unit_price = UoM.compute_price(
self.product.rental_unit, unit_price,
self.unit_price_unit)
unit_price = round_price(unit_price)
return unit_price
@fields.depends(
'rental', '_parent_rental.currency', '_parent_rental.party',
'start', 'company', 'unit', 'product', 'taxes')
def _get_context_rental_price(self):
context = {}
if self.rental:
if self.rental.currency:
context['currency'] = self.rental.currency.id
if self.rental.party:
context['customer'] = self.rental.party.id
if self.start:
context['rental_date'] = self.start
if self.company:
context['company'] = self.company.id
if self.unit:
context['uom'] = self.unit.id
elif self.product:
context['uom'] = self.product.default_uom.id
context['taxes'] = [t.id for t in self.taxes or []]
return context
@fields.depends(methods=['compute_unit_price', 'on_change_with_amount'])
def on_change_quantity(self):
self.unit_price = self.compute_unit_price()
self.amount = self.on_change_with_amount()
@fields.depends(methods=['on_change_quantity'])
def on_change_unit(self):
self.on_change_quantity()
@fields.depends(methods=['compute_unit_price', 'on_change_with_amount'])
def _on_change_planned(self):
self.unit_price = self.compute_unit_price()
self.amount = self.on_change_with_amount()
@fields.depends(methods=['_on_change_planned'])
def on_change_planned_start_day(self):
self._on_change_planned()
@fields.depends(methods=['_on_change_planned'])
def on_change_planned_end_day(self):
self._on_change_planned()
@fields.depends(methods=['_on_change_planned'])
def on_change_planned_start(self):
self._on_change_planned()
@fields.depends(methods=['_on_change_planned'])
def on_change_planned_end(self):
self._on_change_planned()
@fields.depends(
'per_day',
'planned_start_day', 'planned_end_day',
'planned_start', 'planned_end')
def on_change_with_planned_duration(self, name=None):
if self.per_day:
if self.planned_start_day and self.planned_end_day:
return self.planned_end_day - self.planned_start_day
else:
if self.planned_start and self.planned_end:
return self.planned_end - self.planned_start
@property
@fields.depends('planned_duration', 'unit_price_unit')
def planned_duration_unit(self):
pool = Pool()
UoM = pool.get('product.uom')
Data = pool.get('ir.model.data')
hour = UoM(Data.get_id('product', 'uom_hour'))
if self.planned_duration:
duration = self.planned_duration.total_seconds() / 60 / 60
else:
duration = 0
if self.unit_price_unit:
duration = UoM.compute_qty(
hour, duration, self.unit_price_unit, round=False)
return duration
@fields.depends(
'per_day', 'company',
'actual_start', 'planned_start_day', 'planned_start')
def on_change_with_start(self, name=None):
if self.per_day:
return (
to_date(self.actual_start, self.company)
or self.planned_start_day)
else:
return self.actual_start or self.planned_start
@fields.depends(
'per_day', 'company',
'actual_end', 'planned_end_day', 'planned_end')
def on_change_with_end(self, name=None):
if self.per_day:
return (
to_date(self.actual_end, self.company) or self.planned_end_day)
else:
return self.actual_end or self.planned_end
@fields.depends(methods=['on_change_with_start', 'on_change_with_end'])
def on_change_with_duration(self, name=None):
start = self.on_change_with_start()
end = self.on_change_with_end()
if start and end:
return end - start
@property
@fields.depends('duration', 'unit_price_unit')
def duration_unit(self):
pool = Pool()
UoM = pool.get('product.uom')
Data = pool.get('ir.model.data')
hour = UoM(Data.get_id('product', 'uom_hour'))
if self.duration:
duration = self.duration.total_seconds() / 60 / 60
else:
duration = 0
if self.unit_price_unit:
duration = UoM.compute_qty(
hour, duration, self.unit_price_unit, round=False)
return duration
@fields.depends(
'quantity', 'unit_price', 'currency',
methods=['on_change_with_planned_duration', 'planned_duration_unit'])
def on_change_with_planned_amount(self, name=None):
self.planned_duration = self.on_change_with_planned_duration()
if self.quantity is not None and self.unit_price is not None:
amount = (
Decimal(self.quantity)
* Decimal(self.planned_duration_unit)
* self.unit_price)
if self.currency:
amount = self.currency.round(amount)
return amount
@fields.depends(
'quantity', 'unit_price', 'currency',
methods=['on_change_with_duration', 'duration_unit'])
def on_change_with_amount(self, name=None):
self.duration = self.on_change_with_duration()
if self.quantity is not None and self.unit_price is not None:
amount = (
Decimal(self.quantity)
* Decimal(self.duration_unit)
* self.unit_price)
if self.currency:
amount = self.currency.round(amount)
return amount
@classmethod
def get_rental_states(cls):
pool = Pool()
Rental = pool.get('sale.rental')
return Rental.fields_get(['state'])['state']['selection']
@fields.depends(
'rental', '_parent_rental.state', 'actual_start', 'actual_end')
def on_change_with_rental_state(self, name=None):
if self.actual_end:
return 'done'
elif self.actual_start:
return 'picked up'
elif self.rental:
return self.rental.state
@fields.depends('rental', '_parent_rental.company')
def on_change_with_company(self, name=None):
if self.rental and self.rental.company:
return self.rental.company.id
@fields.depends('rental', '_parent_rental.currency')
def on_change_with_currency(self, name=None):
if self.rental and self.rental.currency:
return self.rental.currency.id
@fields.depends('product')
def on_change_with_product_uom_category(self, name=None):
if self.product:
return self.product.default_uom_category.id
@property
def picking_location(self):
return (
self.rental.warehouse.rental_picking_location
or self.rental.warehouse.storage_location)
@property
def return_location(self):
return (
self.rental.warehouse.rental_return_location
or self.rental.warehouse.storage_location)
@property
def rental_location(self):
return self.rental.warehouse.rental_location
def get_moves(self, type):
if self.product.type == 'service':
return []
if type == 'out' and self.outgoing_moves:
return self.outgoing_moves
elif type == 'in' and self.incoming_moves:
return self.incoming_moves
move = self.get_move(type)
move.quantity = self.quantity
move.unit = self.unit
return [move]
def get_move(self, type):
pool = Pool()
Move = pool.get('stock.move')
move = Move()
move.product = self.product
if type == 'out':
move.from_location = self.picking_location
move.to_location = self.rental_location
move.planned_date = to_date(self.start, self.company)
elif type == 'in':
move.from_location = self.rental_location
move.to_location = self.return_location
move.planned_date = to_date(self.end, self.company)
move.state = 'draft'
move.company = self.rental.company
if move.on_change_with_unit_price_required():
move.unit_price = self.unit_price * self.duration_unit
move.currency = self.rental.currency
move.origin = self.rental
return move
@property
def to_invoice(self):
return self.rental_state == 'done' and not self.invoice_lines
@property
def start_invoice(self):
return self.start
@property
def end_invoice(self):
return self.end
@property
def duration_invoice(self):
return self.duration
@property
def duration_unit_invoice(self):
pool = Pool()
UoM = pool.get('product.uom')
Data = pool.get('ir.model.data')
hour = UoM(Data.get_id('product', 'uom_hour'))
if duration := self.duration_invoice:
duration = duration.total_seconds() / 60 / 60
else:
duration = 0
if self.unit_price_unit:
duration = UoM.compute_qty(
hour, duration, self.unit_price_unit, round=False)
return duration
def get_invoice_lines(self):
pool = Pool()
InvoiceLine = pool.get('account.invoice.line')
if self.rental_state != 'done' or self.invoice_lines:
return []
invoice_line = InvoiceLine(invoice_type='out', type='line')
invoice_line.currency = self.rental.currency
invoice_line.company = self.rental.company
invoice_line.origin = self
invoice_line.quantity = self.quantity
invoice_line.unit = self.unit
invoice_line.product = self.product
invoice_line.unit_price = round_price(
self.unit_price * Decimal(self.duration_unit_invoice))
invoice_line.taxes = self.taxes
invoice_line.account = self.product.account_rental_used
return [invoice_line]
def get_rec_name(self, name):
pool = Pool()
Lang = pool.get('ir.lang')
lang = Lang.get()
converter = self.__class__.duration.converter
quantity = lang.format_number_symbol(
self.quantity, self.unit, digits=self.unit.digits)
duration = Report.format_timedelta(
self.duration, converter=converter, lang=lang)
product = self.product.rec_name
rental = self.rental.rec_name
return f'{duration} × {quantity} {product} @ {rental}'
@classmethod
def search_rec_name(cls, name, clause):
_, operator, value = clause
if operator.startswith('!') or operator.startswith('not '):
bool_op = 'AND'
else:
bool_op = 'OR'
return [bool_op,
('rental.rec_name', *clause[1:]),
('product.rec_name', *clause[1:]),
]
@classmethod
def copy(cls, lines, default=None):
default = default.copy() if default is not None else {}
default.setdefault('actual_start')
default.setdefault('actual_end')
default.setdefault('invoice_lines')
return super().copy(lines, default=default)
@classmethod
def check_modification(cls, mode, lines, values=None, external=False):
super().check_modification(
mode, lines, values=values, external=external)
if mode == 'delete':
for line in lines:
if line.rental_state not in {'cancelled', 'draft'}:
raise AccessError(gettext(
'sale_rental.msg_rental_line_delete_cancel_draft',
line=line.rec_name,
rental=line.rental.rec_name))
class RentalLine_Tax(ModelSQL):
__name__ = 'sale.rental.line-account.tax'
line = fields.Many2One(
'sale.rental.line', "Rental Line", ondelete='CASCADE', required=True)
tax = fields.Many2One(
'account.tax', "Tax", ondelete='RESTRICT', required=True)
@classmethod
def __setup__(cls):
super().__setup__()
t = cls.__table__()
cls._sql_constraints += [
('line_tax_unique', Unique(t, t.line, t.tax),
'sale_rental.msg_rental_line_tax_unique'),
]
class RentalLine_Outgoing_Move(ModelSQL):
__name__ = 'sale.rental.line-outgoing-stock.move'
line = fields.Many2One(
'sale.rental.line', "Rental Line", ondelete='CASCADE', required=True)
move = fields.Many2One(
'stock.move', "Move", ondelete='RESTRICT', required=True,
domain=[
('origin', 'like', 'sale.rental,%'),
('to_location.type', '=', 'rental'),
])
class RentalLine_Incoming_Move(ModelSQL):
__name__ = 'sale.rental.line-incoming-stock.move'
line = fields.Many2One(
'sale.rental.line', "Rental Line", ondelete='CASCADE', required=True)
move = fields.Many2One(
'stock.move', "Move", ondelete='RESTRICT', required=True,
domain=[
('origin', 'like', 'sale.rental,%'),
('from_location.type', '=', 'rental'),
])
class _RentalShowLine(ModelView):
rental_line = fields.Many2One(
'sale.rental.line', "Rental Line", readonly=True)
product = fields.Function(
fields.Many2One('product.product', "Product"),
'on_change_with_product')
quantity = fields.Function(
fields.Float("Quantity", digits='unit'),
'on_change_with_quantity')
unit = fields.Function(
fields.Many2One('product.uom', "Unit"),
'on_change_with_unit')
@classmethod
def get(cls, rental_line):
line = cls()
line.rental_line = rental_line
line.product = line.on_change_with_product()
line.quantity = line.on_change_with_quantity()
line.unit = line.on_change_with_unit()
return line
@fields.depends('rental_line')
def on_change_with_product(self, name=None):
if self.rental_line and self.rental_line.product:
return self.rental_line.product.id
@fields.depends('rental_line')
def on_change_with_quantity(self, name=None):
if self.rental_line:
return self.rental_line.quantity
@fields.depends('rental_line')
def on_change_with_unit(self, name=None):
if self.rental_line and self.rental_line.unit:
return self.rental_line.unit.id
def get_moves(self, type, date=None):
if self.rental_line.product.type == 'service':
return []
move = self.rental_line.get_move(type)
move.unit = self.unit
move.effective_date = date
return [move]
class RentalPickup(Wizard):
__name__ = 'sale.rental.pickup'
start = StateTransition()
show = StateView('sale.rental.pickup.show',
'sale_rental.sale_rental_pickup_show_view_form', [
Button("Cancel", 'end', 'tryton-cancel'),
Button("Pickup", 'pickup', 'tryton-ok', default=True),
])
pickup = StateTransition()
@property
def rental_lines(self):
for line in self.record.lines:
if line.rental_state == 'confirmed':
yield line
def transition_start(self):
if any(self.rental_lines):
return 'show'
else:
self.record.try_picked_up()
return 'end'
def default_show(self, fields):
pool = Pool()
Line = pool.get('sale.rental.pickup.show.line')
defaults = {}
if 'start' in fields:
defaults['start'] = min(
map(partial(to_datetime, company=self.record.company),
(l.planned_start or l.planned_start_day
for l in self.rental_lines)),
default=dt.datetime.now())
if 'lines' in fields:
defaults['lines'] = lines = []
for rental_line in self.rental_lines:
lines.append(Line.get(rental_line)._changed_values())
return defaults
def transition_pickup(self):
pool = Pool()
Lang = pool.get('ir.lang')
Line = pool.get('sale.rental.line')
Move = pool.get('stock.move')
lang = Lang.get()
rental_lines = []
moves_to_delete = []
outgoing_moves, incoming_moves = [], []
for line in self.show.lines:
rental_line = line.rental_line
if rental_line in rental_lines:
raise ValidationError(gettext(
'sale_rental.msg_rental_pickup_once',
line=rental_line.rec_name))
rental_lines.append(rental_line)
quantity = line.quantity_picked
unit = rental_line.unit
if not quantity:
continue
if not (0 <= quantity <= rental_line.quantity):
raise ValidationError(gettext(
'sale_rental.msg_rental_pickup_quantity',
line=rental_line.rec_name,
quantity=lang.format_number_symbol(
rental_line.quantity, unit),
picked=lang.format_number_symbol(quantity, unit)))
remaining = unit.round(rental_line.quantity - quantity)
if remaining:
with Transaction().set_context(_sale_rental_line_split=True):
Line.copy([rental_line], default={
'quantity': remaining,
'outgoing_moves': None,
'incoming_moves': None,
})
rental_line.actual_start = self.show.start
rental_line.quantity = quantity
moves_to_delete.extend(rental_line.outgoing_moves)
rental_line.outgoing_moves = line.get_moves(
'out', date=self.show.start.date())
outgoing_moves.extend(rental_line.outgoing_moves)
moves_to_delete.extend(rental_line.incoming_moves)
rental_line.incoming_moves = line.get_moves('in')
incoming_moves.extend(rental_line.incoming_moves)
Move.save(outgoing_moves + incoming_moves)
Line.save(rental_lines)
Move.delete(moves_to_delete)
self.model.set_moves([self.record])
Move.do(outgoing_moves)
return 'end'
class RentalPickupShow(ModelView):
__name__ = 'sale.rental.pickup.show'
start = fields.DateTime("Start", format=_datetime_format, required=True)
lines = fields.One2Many('sale.rental.pickup.show.line', 'parent', "Lines")
class RentalPickupShowLine(_RentalShowLine):
__name__ = 'sale.rental.pickup.show.line'
parent = fields.Many2One(
'sale.rental.pickup.show', "Parent", readonly=True)
quantity_picked = fields.Float(
"Quantity Picked", digits='unit', required=True,
domain=[
('quantity_picked', '<=', Eval('quantity')),
('quantity_picked', '>=', 0),
])
@classmethod
def get(cls, rental_line):
line = super().get(rental_line)
line.quantity_picked = 0
return line
def get_moves(self, type, date=None):
moves = super().get_moves(type, date=date)
for move in moves:
move.quantity = self.quantity_picked
return moves
class RentalReturn(Wizard):
__name__ = 'sale.rental.return'
start = StateTransition()
show = StateView('sale.rental.return.show',
'sale_rental.sale_rental_return_show_view_form', [
Button("Cancel", 'end', 'tryton-cancel'),
Button("Return", 'return_', 'tryton-ok', default=True),
])
return_ = StateTransition()
@property
def rental_lines(self):
for line in self.record.lines:
if line.rental_state == 'picked up':
yield line
def transition_start(self):
if any(self.rental_lines):
return 'show'
else:
self.record.try_done()
return 'end'
def default_show(self, fields):
pool = Pool()
Line = pool.get('sale.rental.return.show.line')
defaults = {}
if 'end' in fields:
defaults['end'] = max(
map(partial(
to_datetime, company=self.record.company,
time=_time_max),
(l.planned_end or l.planned_end_day
for l in self.rental_lines)),
default=dt.datetime.now())
if 'lines' in fields:
defaults['lines'] = lines = []
for rental_line in self.rental_lines:
lines.append(Line.get(rental_line)._changed_values())
return defaults
def transition_return_(self):
pool = Pool()
Lang = pool.get('ir.lang')
Line = pool.get('sale.rental.line')
Move = pool.get('stock.move')
lang = Lang.get()
rental_lines = []
moves_to_delete = []
incoming_moves = []
for line in self.show.lines:
rental_line = line.rental_line
if rental_line in rental_lines:
raise ValidationError(gettext(
'sale_rental.msg_rental_return_once',
line=rental_line.rec_name))
rental_lines.append(rental_line)
quantity = line.quantity_returned
unit = rental_line.unit
if not quantity:
continue
if not (0 <= quantity <= rental_line.quantity):
raise ValidationError(gettext(
'sale_rental.msg_rental_return_quantity',
line=rental_line.rec_name,
quantity=lang.format_number_symbol(
rental_line.quantity, unit),
returned=lang.format_number_symbol(quantity, unit)))
remaining = unit.round(rental_line.quantity - quantity)
if remaining:
with Transaction().set_context(_sale_rental_line_split=True):
Line.copy([rental_line], default={
'quantity': remaining,
'actual_start': rental_line.actual_start,
'incoming_moves': None,
})
rental_line.actual_end = self.show.end
rental_line.quantity = quantity
moves_to_delete.extend(rental_line.incoming_moves)
rental_line.incoming_moves = line.get_moves(
'in', date=self.show.end.date())
incoming_moves.extend(rental_line.incoming_moves)
Move.save(incoming_moves)
Line.save(rental_lines)
Move.delete(moves_to_delete)
self.model.set_moves([self.record])
Move.do(incoming_moves)
return 'end'
class RentalReturnShow(ModelView):
__name__ = 'sale.rental.return.show'
end = fields.DateTime("End", format=_datetime_format, required=True)
lines = fields.One2Many('sale.rental.return.show.line', 'parent', "Lines")
class RentalReturnShowLine(_RentalShowLine):
__name__ = 'sale.rental.return.show.line'
parent = fields.Many2One(
'sale.rental.return.show', "Parent", readonly=True)
quantity_returned = fields.Float(
"Quantity Returned", digits='unit', required=True,
domain=[
('quantity_returned', '<=', Eval('quantity')),
('quantity_returned', '>=', 0),
])
@classmethod
def get(cls, rental_line):
line = super().get(rental_line)
line.quantity_returned = 0
return line
def get_moves(self, type, date=None):
moves = super().get_moves(type, date=date)
for move in moves:
move.quantity = self.quantity_returned
return moves