mirror of
https://github.com/OCA/rma.git
synced 2025-02-16 17:11:47 +02:00
409 lines
17 KiB
Python
409 lines
17 KiB
Python
# -*- coding: utf-8 -*-
|
|
# © 2015 Vauxoo
|
|
# © 2013 Camptocamp
|
|
# © 2009-2013 Akretion,
|
|
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
|
|
|
|
import calendar
|
|
import math
|
|
from datetime import datetime
|
|
|
|
from dateutil.relativedelta import relativedelta
|
|
|
|
from openerp import _, api, exceptions, fields, models
|
|
from openerp.tools import (DEFAULT_SERVER_DATE_FORMAT,
|
|
DEFAULT_SERVER_DATETIME_FORMAT)
|
|
|
|
from .invoice_no_date import InvoiceNoDate
|
|
from .product_no_supplier import ProductNoSupplier
|
|
|
|
|
|
class ClaimLine(models.Model):
|
|
|
|
_name = "claim.line"
|
|
|
|
_inherit = 'mail.thread'
|
|
_description = "List of product to return"
|
|
_rec_name = "display_name"
|
|
|
|
SUBJECT_LIST = [('none', 'Not specified'),
|
|
('legal', 'Legal retractation'),
|
|
('cancellation', 'Order cancellation'),
|
|
('damaged', 'Damaged delivered product'),
|
|
('error', 'Shipping error'),
|
|
('exchange', 'Exchange request'),
|
|
('lost', 'Lost during transport'),
|
|
('perfect_conditions',
|
|
'Perfect Conditions'),
|
|
('imperfection', 'Imperfection'),
|
|
('physical_damage_client',
|
|
'Physical Damage by Client'),
|
|
('physical_damage_company',
|
|
'Physical Damage by Company'),
|
|
('other', 'Other')]
|
|
WARRANT_COMMENT = [
|
|
('valid', _("Valid")),
|
|
('expired', _("Expired")),
|
|
('not_define', _("Not Defined"))]
|
|
|
|
number = fields.Char(
|
|
readonly=True,
|
|
default='/',
|
|
help='Claim Line Identification Number')
|
|
company_id = fields.Many2one(
|
|
'res.company', string='Company', readonly=False,
|
|
change_default=True,
|
|
default=lambda self: self.env['res.company']._company_default_get(
|
|
'claim.line'))
|
|
date = fields.Date('Claim Line Date',
|
|
select=True,
|
|
default=fields.date.today())
|
|
name = fields.Char('Description', default='none', required=True,
|
|
help="More precise description of the problem")
|
|
priority = fields.Selection([('0_not_define', 'Not Define'),
|
|
('1_normal', 'Normal'),
|
|
('2_high', 'High'),
|
|
('3_very_high', 'Very High')],
|
|
'Priority', default='0_not_define',
|
|
compute='_compute_priority',
|
|
store=True,
|
|
readonly=False,
|
|
help="Priority attention of claim line")
|
|
claim_diagnosis = fields.\
|
|
Selection([('damaged', 'Product Damaged'),
|
|
('repaired', 'Product Repaired'),
|
|
('good', 'Product in good condition'),
|
|
('hidden', 'Product with hidden physical damage'),
|
|
],
|
|
help="To describe the line product diagnosis")
|
|
claim_origin = fields.Selection(SUBJECT_LIST, 'Claim Subject',
|
|
required=True, help="To describe the "
|
|
"line product problem")
|
|
product_id = fields.Many2one('product.product', string='Product',
|
|
help="Returned product")
|
|
product_returned_quantity = \
|
|
fields.Float('Quantity', digits=(12, 2),
|
|
help="Quantity of product returned")
|
|
unit_sale_price = fields.Float(digits=(12, 2),
|
|
help="Unit sale price of the product. "
|
|
"Auto filled if retrun done "
|
|
"by invoice selection. Be careful "
|
|
"and check the automatic "
|
|
"value as don't take into account "
|
|
"previous refunds, invoice "
|
|
"discount, can be for 0 if product "
|
|
"for free,...")
|
|
return_value = fields.Float(compute='_compute_line_total_amount',
|
|
string='Total return',
|
|
help="Quantity returned * Unit sold price",)
|
|
prodlot_id = fields.Many2one('stock.production.lot',
|
|
string='Serial/Lot number',
|
|
help="The serial/lot of "
|
|
"the returned product")
|
|
applicable_guarantee = fields.Selection([('us', 'Company'),
|
|
('supplier', 'Supplier'),
|
|
('brand', 'Brand manufacturer')],
|
|
'Warranty type')
|
|
guarantee_limit = fields.Date('Warranty limit', readonly=True,
|
|
help="The warranty limit is "
|
|
"computed as: invoice date + warranty "
|
|
"defined on selected product.")
|
|
warning = fields.Selection(WARRANT_COMMENT,
|
|
'Warranty', readonly=True,
|
|
help="If warranty has expired")
|
|
display_name = fields.Char('Name', compute='_get_display_name')
|
|
|
|
@api.model
|
|
def get_warranty_return_partner(self):
|
|
return self.env['product.supplierinfo']._columns[
|
|
'warranty_return_partner'
|
|
].selection
|
|
|
|
warranty_type = fields.Selection(
|
|
get_warranty_return_partner, readonly=True,
|
|
help="Who is in charge of the warranty return treatment towards "
|
|
"the end customer. Company will use the current company "
|
|
"delivery or default address and so on for supplier and brand "
|
|
"manufacturer. Does not necessarily mean that the warranty "
|
|
"to be applied is the one of the return partner (ie: can be "
|
|
"returned to the company and be under the brand warranty")
|
|
warranty_return_partner = \
|
|
fields.Many2one('res.partner', string='Warranty Address',
|
|
help="Where the customer has to "
|
|
"send back the product(s)")
|
|
claim_id = fields.Many2one('crm.claim', string='Related claim',
|
|
ondelete='cascade',
|
|
help="To link to the case.claim object")
|
|
state = fields.Selection([('draft', 'Draft'), ('refused', 'Refused'),
|
|
('confirmed', 'Confirmed, waiting for product'),
|
|
('in_to_control', 'Received, to control'),
|
|
('in_to_treate', 'Controlled, to treate'),
|
|
('treated', 'Treated')],
|
|
string='State', default='draft')
|
|
substate_id = fields.Many2one('substate.substate', string='Sub state',
|
|
help="Select a sub state to precise the "
|
|
"standard state. Example 1: "
|
|
"state = refused; substate could "
|
|
"be warranty over, not in "
|
|
"warranty, no problem,... . "
|
|
"Example 2: state = to treate; "
|
|
"substate could be to refund, to "
|
|
"exchange, to repair,...")
|
|
last_state_change = fields.Date(string='Last change', help="To set the"
|
|
"last state / substate change")
|
|
invoice_line_id = fields.Many2one('account.invoice.line',
|
|
string='Invoice Line',
|
|
help='The invoice line related'
|
|
' to the returned product')
|
|
refund_line_id = fields.Many2one('account.invoice.line',
|
|
string='Refund Line',
|
|
help='The refund line related'
|
|
' to the returned product')
|
|
move_in_id = fields.Many2one('stock.move',
|
|
string='Move Line from picking in',
|
|
help='The move line related'
|
|
' to the returned product')
|
|
move_out_id = fields.Many2one('stock.move',
|
|
string='Move Line from picking out',
|
|
help='The move line related'
|
|
' to the returned product')
|
|
location_dest_id = fields.Many2one('stock.location',
|
|
string='Return Stock Location',
|
|
help='The return stock location'
|
|
' of the returned product')
|
|
claim_type = fields.Many2one(related='claim_id.claim_type',
|
|
string="Claim Line Type",
|
|
store=True, help="Claim classification")
|
|
invoice_date = fields.Datetime(related='invoice_line_id.invoice_id.'
|
|
'create_date',
|
|
help="Date of Claim Invoice")
|
|
|
|
# Method to calculate total amount of the line : qty*UP
|
|
@api.multi
|
|
def _compute_line_total_amount(self):
|
|
for line in self:
|
|
line.return_value = (line.unit_sale_price *
|
|
line.product_returned_quantity)
|
|
|
|
@api.multi
|
|
def copy(self, default=None):
|
|
self.ensure_one()
|
|
default = default or {}
|
|
std_default = {
|
|
'move_in_id': False,
|
|
'move_out_id': False,
|
|
'refund_line_id': False,
|
|
}
|
|
std_default.update(default)
|
|
return super(ClaimLine, self).copy(default=std_default)
|
|
|
|
@api.depends('invoice_date', 'date')
|
|
def _compute_priority(self):
|
|
"""
|
|
To determine the priority of claim line
|
|
"""
|
|
for line_id in self:
|
|
if line_id.invoice_date:
|
|
days = fields.datetime.strptime(line_id.date, '%Y-%m-%d') - \
|
|
fields.datetime.strptime(line_id.invoice_date,
|
|
DEFAULT_SERVER_DATETIME_FORMAT)
|
|
if days.days <= 1:
|
|
line_id.priority = '3_very_high'
|
|
elif days.days <= 7:
|
|
line_id.priority = '2_high'
|
|
else:
|
|
line_id.priority = '1_normal'
|
|
|
|
def _get_subject(self, num):
|
|
if num > 0 and num <= len(self.SUBJECT_LIST):
|
|
return self.SUBJECT_LIST[num - 1][0]
|
|
else:
|
|
return self.SUBJECT_LIST[0][0]
|
|
|
|
@staticmethod
|
|
def warranty_limit(start, warranty_duration):
|
|
""" Take a duration in float, return the duration in relativedelta
|
|
|
|
``relative_delta(months=...)`` only accepts integers.
|
|
We have to extract the decimal part, and then, extend the delta with
|
|
days.
|
|
|
|
"""
|
|
decimal_part, months = math.modf(warranty_duration)
|
|
months = int(months)
|
|
# If we have a decimal part, we add the number them as days to
|
|
# the limit. We need to get the month to know the number of
|
|
# days.
|
|
delta = relativedelta(months=months)
|
|
monthday = start + delta
|
|
__, days_month = calendar.monthrange(monthday.year, monthday.month)
|
|
# ignore the rest of the days (hours) since we expect a date
|
|
days = int(days_month * decimal_part)
|
|
return start + relativedelta(months=months, days=days)
|
|
|
|
def _warranty_limit_values(self, invoice, claim_type, product, claim_date):
|
|
if not (invoice and claim_type and product and claim_date):
|
|
return {'guarantee_limit': False, 'warning': False}
|
|
|
|
invoice_date = invoice.create_date
|
|
if not invoice_date:
|
|
raise InvoiceNoDate
|
|
|
|
warning = 'not_define'
|
|
invoice_date = datetime.strptime(invoice_date,
|
|
DEFAULT_SERVER_DATETIME_FORMAT)
|
|
|
|
if isinstance(claim_type, self.env['crm.claim.type'].__class__):
|
|
claim_type = claim_type.id
|
|
|
|
if claim_type == self.env.ref('crm_claim_type.'
|
|
'crm_claim_type_supplier').id:
|
|
try:
|
|
warranty_duration = product.seller_ids[0].warranty_duration
|
|
except IndexError:
|
|
raise ProductNoSupplier
|
|
else:
|
|
warranty_duration = product.warranty
|
|
|
|
limit = self.warranty_limit(invoice_date, warranty_duration)
|
|
if warranty_duration > 0:
|
|
claim_date = datetime.strptime(claim_date,
|
|
DEFAULT_SERVER_DATETIME_FORMAT)
|
|
if limit < claim_date:
|
|
warning = 'expired'
|
|
else:
|
|
warning = 'valid'
|
|
|
|
return {'guarantee_limit': limit.strftime(DEFAULT_SERVER_DATE_FORMAT),
|
|
'warning': warning}
|
|
|
|
def set_warranty_limit(self):
|
|
self.ensure_one()
|
|
|
|
claim = self.claim_id
|
|
invoice_id = self.invoice_line_id and self.invoice_line_id.invoice_id \
|
|
or claim.invoice_id
|
|
try:
|
|
values = self._warranty_limit_values(
|
|
invoice_id, claim.claim_type,
|
|
self.product_id, claim.date)
|
|
except InvoiceNoDate:
|
|
raise exceptions.UserError(
|
|
_('Cannot find any date for invoice. '
|
|
'Must be a validated invoice.')
|
|
)
|
|
except ProductNoSupplier:
|
|
raise exceptions.UserError(
|
|
_('The product has no supplier configured.')
|
|
)
|
|
|
|
self.write(values)
|
|
return True
|
|
|
|
@api.model
|
|
def auto_set_warranty(self):
|
|
""" Set warranty automatically
|
|
if the user has not himself pressed on 'Calculate warranty state'
|
|
button, it sets warranty for him"""
|
|
for line in self:
|
|
if not line.warning:
|
|
line.set_warranty()
|
|
return True
|
|
|
|
@api.returns('stock.location')
|
|
def get_destination_location(self, product_id, warehouse_id):
|
|
""" Compute and return the destination location to take
|
|
for a return. Always take 'Supplier' one when return type different
|
|
from company.
|
|
"""
|
|
location_dest_id = warehouse_id.lot_stock_id
|
|
|
|
if product_id.seller_ids:
|
|
seller = product_id.seller_ids[0]
|
|
if seller.warranty_return_partner != 'company' \
|
|
and seller.name and \
|
|
seller.name.property_stock_supplier:
|
|
location_dest_id = seller.name.property_stock_supplier
|
|
|
|
return location_dest_id
|
|
|
|
def _warranty_return_address_values(self, product, company, warehouse):
|
|
""" Return the partner to be used as return destination and
|
|
the destination stock location of the line in case of return.
|
|
|
|
We can have various cases here:
|
|
- company or other: return to company partner or
|
|
crm_return_address_id if specified
|
|
- supplier: return to the supplier address
|
|
"""
|
|
if not (product and company and warehouse):
|
|
return {
|
|
'warranty_return_partner': False,
|
|
'warranty_type': False,
|
|
'location_dest_id': False
|
|
}
|
|
sellers = product.seller_ids
|
|
if sellers:
|
|
seller = sellers[0]
|
|
return_address_id = seller.warranty_return_address.id
|
|
return_type = seller.warranty_return_partner
|
|
else:
|
|
# when no supplier is configured, returns to the company
|
|
return_address = (company.crm_return_address_id or
|
|
company.partner_id)
|
|
return_address_id = return_address.id
|
|
return_type = 'company'
|
|
location_dest = self.get_destination_location(product, warehouse)
|
|
return {
|
|
'warranty_return_partner': return_address_id,
|
|
'warranty_type': return_type,
|
|
'location_dest_id': location_dest.id
|
|
}
|
|
|
|
def set_warranty_return_address(self):
|
|
self.ensure_one()
|
|
claim = self.claim_id
|
|
values = self._warranty_return_address_values(
|
|
self.product_id, claim.company_id, claim.warehouse_id)
|
|
self.write(values)
|
|
return True
|
|
|
|
@api.multi
|
|
def set_warranty(self):
|
|
""" Calculate warranty limit and address
|
|
"""
|
|
for line_id in self:
|
|
if not line_id.product_id:
|
|
raise exceptions.UserError(_('Please set product first'))
|
|
|
|
if not line_id.invoice_line_id:
|
|
raise exceptions.UserError(_('Please set invoice first'))
|
|
|
|
line_id.set_warranty_limit()
|
|
line_id.set_warranty_return_address()
|
|
|
|
@api.model
|
|
def _get_sequence_number(self):
|
|
""" Return the value of the sequence for the number field in the
|
|
claim.line model.
|
|
"""
|
|
return self.env['ir.sequence'].next_by_code('claim.line')
|
|
|
|
@api.model
|
|
def create(self, vals):
|
|
"""Return write the identify number once the claim line is create.
|
|
"""
|
|
vals = vals or {}
|
|
|
|
if ('number' not in vals) or (vals.get('number', False) == '/'):
|
|
vals['number'] = self._get_sequence_number()
|
|
|
|
res = super(ClaimLine, self).create(vals)
|
|
return res
|
|
|
|
@api.multi
|
|
def _get_display_name(self):
|
|
for line_id in self:
|
|
line_id.display_name = "%s - %s" % (
|
|
line_id.claim_id.code, line_id.name)
|