first commit
This commit is contained in:
363
modules/sale_rental/product.py
Normal file
363
modules/sale_rental/product.py
Normal file
@@ -0,0 +1,363 @@
|
||||
# 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 itertools import chain
|
||||
|
||||
from trytond.i18n import gettext
|
||||
from trytond.model import (
|
||||
MatchMixin, ModelSQL, ModelView, fields, sequence_ordered)
|
||||
from trytond.modules.account_product.exceptions import TaxError
|
||||
from trytond.modules.account_product.product import (
|
||||
account_used, template_property)
|
||||
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 Eval, Id, If, TimeDelta
|
||||
from trytond.transaction import Transaction
|
||||
|
||||
|
||||
class Category(metaclass=PoolMeta):
|
||||
__name__ = 'product.category'
|
||||
|
||||
account_rental = fields.MultiValue(fields.Many2One(
|
||||
'account.account', "Account Rental",
|
||||
domain=[
|
||||
('closed', '!=', True),
|
||||
('type.revenue', '=', True),
|
||||
('company', '=', Eval('context', {}).get('company', -1)),
|
||||
],
|
||||
states={
|
||||
'invisible': (
|
||||
~Eval('context', {}).get('company', -1)
|
||||
| Eval('account_parent', True)
|
||||
| ~Eval('accounting', False)),
|
||||
}))
|
||||
customer_rental_taxes = fields.Many2Many(
|
||||
'product.category-customer_rental-account.tax',
|
||||
'category', 'tax', "Customer Rental Taxes",
|
||||
order=[('tax.sequence', 'ASC'), ('tax.id', 'ASC')],
|
||||
domain=[
|
||||
('parent', '=', None),
|
||||
['OR',
|
||||
('group', '=', None),
|
||||
('group.kind', 'in', ['sale', 'both']),
|
||||
],
|
||||
],
|
||||
states={
|
||||
'invisible': (
|
||||
~Eval('context', {}).get('company')
|
||||
| Eval('taxes_parent', False)
|
||||
| ~Eval('accounting', False)),
|
||||
},
|
||||
help="The taxes to apply when renting goods or services "
|
||||
"of this category.")
|
||||
customer_rental_taxes_used = fields.Function(
|
||||
fields.Many2Many(
|
||||
'account.tax', None, None, "Customer Rental Taxes Used"),
|
||||
'get_taxes')
|
||||
|
||||
@classmethod
|
||||
def multivalue_model(cls, field):
|
||||
pool = Pool()
|
||||
if field == 'account_rental':
|
||||
return pool.get('product.category.account')
|
||||
return super().multivalue_model(field)
|
||||
|
||||
@property
|
||||
@account_used('account_rental')
|
||||
def account_rental_used(self):
|
||||
pass
|
||||
|
||||
|
||||
class CategoryAccount(metaclass=PoolMeta):
|
||||
__name__ = 'product.category.account'
|
||||
|
||||
account_rental = fields.Many2One(
|
||||
'account.account', "Account Rental",
|
||||
domain=[
|
||||
('closed', '!=', True),
|
||||
('type.revenue', '=', True),
|
||||
('company', '=', Eval('company', -1)),
|
||||
])
|
||||
|
||||
|
||||
class CategoryCustomerRentalTax(ModelSQL):
|
||||
"Category - Customer Rental Tax"
|
||||
__name__ = 'product.category-customer_rental-account.tax'
|
||||
category = fields.Many2One(
|
||||
'product.category', "Category", ondelete='CASCADE', required=True)
|
||||
tax = fields.Many2One(
|
||||
'account.tax', "Tax", ondelete='RESTRICT', required=True)
|
||||
|
||||
@classmethod
|
||||
def __setup__(cls):
|
||||
super().__setup__()
|
||||
cls.__access__.add('tax')
|
||||
|
||||
|
||||
class Template(metaclass=PoolMeta):
|
||||
__name__ = 'product.template'
|
||||
|
||||
rentable = fields.Boolean(
|
||||
"Rentable",
|
||||
states={
|
||||
'invisible': ~Eval('type').in_(['assets', 'service']),
|
||||
})
|
||||
rental_unit = fields.Many2One(
|
||||
'product.uom', "Rental Unit",
|
||||
domain=[
|
||||
('category', '=', Id('product', 'uom_cat_time')),
|
||||
],
|
||||
states={
|
||||
'invisible': ~Eval('rentable', False),
|
||||
'required': Eval('rentable', False),
|
||||
})
|
||||
rental_per_day = fields.Boolean(
|
||||
"Rental per Day",
|
||||
states={
|
||||
'invisible': ~Eval('rentable', False),
|
||||
})
|
||||
rental_prices = fields.One2Many(
|
||||
'product.rental.price', 'template', "Rental Prices",
|
||||
states={
|
||||
'invisible': ~Eval('rentable', False),
|
||||
})
|
||||
|
||||
@classmethod
|
||||
def default_rental_per_day(cls):
|
||||
return False
|
||||
|
||||
@property
|
||||
@fields.depends('account_category', methods=['get_account'])
|
||||
@account_used('account_rental', 'account_category')
|
||||
def account_rental_used(self):
|
||||
pass
|
||||
|
||||
@property
|
||||
@fields.depends(methods=['get_taxes', 'account_rental_used'])
|
||||
def customer_rental_taxes_used(self):
|
||||
taxes = self.get_taxes('customer_rental_taxes_used')
|
||||
if taxes is None:
|
||||
account = self.account_rental_used
|
||||
if account:
|
||||
taxes = account.taxes
|
||||
if taxes is None:
|
||||
# Allow empty values on on_change
|
||||
if Transaction().readonly:
|
||||
taxes = []
|
||||
else:
|
||||
raise TaxError(
|
||||
gettext('account_product.msg_missing_taxes',
|
||||
name=self.rec_name))
|
||||
return taxes
|
||||
|
||||
@classmethod
|
||||
def view_attributes(cls):
|
||||
return super().view_attributes() + [
|
||||
('//page[@id="rental"]', 'states', {
|
||||
'invisible': ~Eval('rentable', False),
|
||||
})]
|
||||
|
||||
@classmethod
|
||||
def copy(cls, templates, default=None):
|
||||
pool = Pool()
|
||||
RentalPrice = pool.get('product.rental.price')
|
||||
default = default.copy() if default is not None else {}
|
||||
|
||||
copy_rental_prices = 'rental_prices' not in default
|
||||
default.setdefault('rental_prices', None)
|
||||
new_templates = super().copy(templates, default=default)
|
||||
if copy_rental_prices:
|
||||
old2new = {}
|
||||
to_copy = []
|
||||
for template, new_template in zip(templates, new_templates):
|
||||
to_copy.extend(
|
||||
rp for rp in template.rental_prices if not rp.product)
|
||||
old2new[template.id] = new_template.id
|
||||
if to_copy:
|
||||
RentalPrice.copy(to_copy, {
|
||||
'template': lambda d: old2new[d['template']],
|
||||
})
|
||||
return new_templates
|
||||
|
||||
|
||||
class Product(metaclass=PoolMeta):
|
||||
__name__ = 'product.product'
|
||||
|
||||
rental_prices = fields.One2Many(
|
||||
'product.rental.price', 'product', "Rental Prices",
|
||||
domain=[
|
||||
('template', '=', Eval('template', -1)),
|
||||
],
|
||||
states={
|
||||
'invisible': ~Eval('rentable', False),
|
||||
})
|
||||
rental_price_uom = fields.Function(
|
||||
Monetary("Rental Price", digits=price_digits),
|
||||
'get_rental_price_uom')
|
||||
account_rental_used = template_property('account_rental_used')
|
||||
customer_rental_taxes_used = template_property(
|
||||
'customer_rental_taxes_used')
|
||||
|
||||
@classmethod
|
||||
def get_rental_price_uom(cls, products, name):
|
||||
context = Transaction().context
|
||||
quantity = context.get('quantity') or 0
|
||||
duration = context.get('duration') or dt.timedelta()
|
||||
return cls.get_rental_price(
|
||||
products, quantity=quantity, duration=duration)
|
||||
|
||||
@classmethod
|
||||
def get_rental_price(cls, products, quantity=0, duration=None):
|
||||
"""
|
||||
Return the rental price for products, quantity and duration.
|
||||
It uses if set in the context:
|
||||
uom: the unit of measure or the sale uom of the product
|
||||
currency: the currency id for the returned price
|
||||
"""
|
||||
pool = Pool()
|
||||
Currency = pool.get('currency.currency')
|
||||
Date = pool.get('ir.date')
|
||||
UoM = pool.get('product.uom')
|
||||
User = pool.get('res.user')
|
||||
|
||||
transaction = Transaction()
|
||||
context = transaction.context
|
||||
today = Date.today()
|
||||
prices = {}
|
||||
|
||||
assert len(products) == len(set(products))
|
||||
|
||||
uom = None
|
||||
if context.get('uom'):
|
||||
uom = UoM(context['uom'])
|
||||
currency = None
|
||||
if context.get('currency'):
|
||||
currency = Currency(context['currency'])
|
||||
user = User(transaction.user)
|
||||
date = context.get('rental_date') or today
|
||||
|
||||
for product in products:
|
||||
unit_price = product._get_rental_unit_price(
|
||||
quantity=quantity, duration=duration, company=user.company.id)
|
||||
if unit_price is not None:
|
||||
if uom and product.default_uom.category == uom.category:
|
||||
unit_price = UoM.compute_price(
|
||||
product.default_uom, unit_price, uom)
|
||||
if currency and user.company:
|
||||
if user.company.currency != currency:
|
||||
with transaction.set_context(date=date):
|
||||
unit_price = Currency.compute(
|
||||
user.company.currency, unit_price,
|
||||
currency, round=False)
|
||||
unit_price = round_price(unit_price)
|
||||
prices[product.id] = unit_price
|
||||
return prices
|
||||
|
||||
def _get_rental_unit_price(self, quantity=0, duration=None, **pattern):
|
||||
for price in chain(self.rental_prices, self.template.rental_prices):
|
||||
if price.match(quantity, duration, pattern):
|
||||
return price.get_unit_price(self.rental_unit)
|
||||
|
||||
@classmethod
|
||||
def copy(cls, products, default=None):
|
||||
pool = Pool()
|
||||
RentalPrice = pool.get('product.rental.price')
|
||||
default = default.copy() if default is not None else {}
|
||||
|
||||
copy_rental_prices = 'rental_prices' not in default
|
||||
if 'template' in default:
|
||||
default.setdefault('rental_prices', None)
|
||||
new_products = super().copy(products, default=default)
|
||||
if 'template' in default and copy_rental_prices:
|
||||
template2new = {}
|
||||
product2new = {}
|
||||
to_copy = []
|
||||
for product, new_product in zip(products, new_products):
|
||||
if product.rental_prices:
|
||||
to_copy.extend(product.rental_prices)
|
||||
template2new[product.template.id] = new_product.template.id
|
||||
product2new[product.id] = new_product.id
|
||||
if to_copy:
|
||||
RentalPrice.copy(to_copy, {
|
||||
'product': lambda d: product2new[d['product']],
|
||||
'template': lambda d: template2new[d['template']],
|
||||
})
|
||||
return new_products
|
||||
|
||||
|
||||
class RentalPrice(sequence_ordered(), ModelSQL, ModelView, MatchMixin):
|
||||
__name__ = 'product.rental.price'
|
||||
|
||||
template = fields.Many2One(
|
||||
'product.template', "Product", required=True, ondelete='CASCADE',
|
||||
domain=[
|
||||
If(Eval('product'),
|
||||
('products', '=', Eval('product', -1)),
|
||||
()),
|
||||
],
|
||||
context={
|
||||
'company': Eval('company', -1),
|
||||
},
|
||||
depends=['company'])
|
||||
product = fields.Many2One(
|
||||
'product.product', "Variant",
|
||||
domain=[
|
||||
If(Eval('template'),
|
||||
('template', '=', Eval('template', -1)),
|
||||
()),
|
||||
],
|
||||
context={
|
||||
'company': Eval('company', -1),
|
||||
},
|
||||
depends=['company'])
|
||||
company = fields.Many2One(
|
||||
'company.company', "Company", required=True, ondelete='CASCADE')
|
||||
duration = fields.TimeDelta(
|
||||
"Duration", required=True,
|
||||
domain=[
|
||||
('duration', '>', TimeDelta()),
|
||||
],
|
||||
help="The minimal duration to apply the price.")
|
||||
price = Monetary(
|
||||
"Price", currency='currency', digits=price_digits, required=True)
|
||||
|
||||
currency = fields.Function(
|
||||
fields.Many2One('currency.currency', "Currency"),
|
||||
'on_change_with_currency')
|
||||
|
||||
@classmethod
|
||||
def __setup__(cls):
|
||||
super().__setup__()
|
||||
cls.__access__.add('template')
|
||||
cls._order.insert(0, ('company', 'ASC'))
|
||||
|
||||
@classmethod
|
||||
def default_company(cls):
|
||||
return Transaction().context.get('company')
|
||||
|
||||
@fields.depends('product', '_parent_product.template')
|
||||
def on_change_product(self):
|
||||
if self.product:
|
||||
self.template = self.product.template
|
||||
|
||||
@fields.depends('company')
|
||||
def on_change_with_currency(self, name=None):
|
||||
if self.company:
|
||||
return self.company.currency.id
|
||||
|
||||
def match(self, quantity, duration, pattern, match_none=False):
|
||||
if self.duration > duration:
|
||||
return False
|
||||
return super().match(pattern, match_none=match_none)
|
||||
|
||||
def get_unit_price(self, unit):
|
||||
pool = Pool()
|
||||
UoM = pool.get('product.uom')
|
||||
Data = pool.get('ir.model.data')
|
||||
hour = UoM(Data.get_id('product', 'uom_hour'))
|
||||
unit_price = (
|
||||
self.price / Decimal(self.duration.total_seconds() / 60 / 60))
|
||||
return UoM.compute_price(hour, unit_price, unit)
|
||||
Reference in New Issue
Block a user