diff --git a/crm_claim_rma/__openerp__.py b/crm_claim_rma/__openerp__.py index 2487ed12..e441d1cf 100644 --- a/crm_claim_rma/__openerp__.py +++ b/crm_claim_rma/__openerp__.py @@ -43,6 +43,8 @@ It mainly contains the following features: * product refund * access to related customer data (orders, invoices, refunds, picking in/out) from a claim +* use the OpenERP chatter within team like in opportunity (reply to refer to + the team, not a person) Using this module makes the logistic flow of return this way: @@ -63,6 +65,7 @@ Contributors: * BenoƮt Guillot * Joel Grand-Guillaume * Guewen Baconnier + * Yannick Vaucher """, 'author': 'Akretion, Camptocamp', @@ -77,6 +80,7 @@ Contributors: 'security/ir.model.access.csv', 'account_invoice_view.xml', 'stock_view.xml', + 'res_partner_view.xml', 'crm_claim_rma_data.xml', ], 'images': ['images/product_return.png', diff --git a/crm_claim_rma/account_invoice.py b/crm_claim_rma/account_invoice.py index 4b2db22f..b6ca9285 100644 --- a/crm_claim_rma/account_invoice.py +++ b/crm_claim_rma/account_invoice.py @@ -42,7 +42,7 @@ class account_invoice(orm.Model): claim_line_obj = self.pool.get('claim.line') # check if is an invoice_line and we are from a claim if not (context.get('claim_line_ids') and lines and - lines[0]._name =='account.invoice.line'): + lines[0]._name == 'account.invoice.line'): return super(account_invoice, self)._refund_cleanup_lines( cr, uid, lines, context=None) diff --git a/crm_claim_rma/crm_claim_rma.py b/crm_claim_rma/crm_claim_rma.py index b818a4be..fdd9760c 100644 --- a/crm_claim_rma/crm_claim_rma.py +++ b/crm_claim_rma/crm_claim_rma.py @@ -23,12 +23,13 @@ import calendar import math -from openerp.osv import fields, orm +from openerp.osv import fields, orm, osv from datetime import datetime from dateutil.relativedelta import relativedelta from openerp.tools import (DEFAULT_SERVER_DATE_FORMAT, DEFAULT_SERVER_DATETIME_FORMAT) from openerp.tools.translate import _ +from openerp import SUPERUSER_ID class substate_substate(orm.Model): @@ -106,13 +107,13 @@ class claim_line(orm.Model): 'product_returned_quantity': fields.float( 'Quantity', digits=(12, 2), help="Quantity of product returned"), - 'unit_sale_price' : fields.float( + 'unit_sale_price': fields.float( 'Unit sale price', 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.function( + 'return_value': fields.function( _line_total_amount, string='Total return', type='float', help="Quantity returned * Unit sold price",), 'prodlot_id': fields.many2one( @@ -133,23 +134,24 @@ class claim_line(orm.Model): 'Warranty', readonly=True, help="If warranty has expired"), - "warranty_type": fields.selection( + 'warranty_type': fields.selection( get_warranty_return_partner, 'Warranty type', 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( + 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', help="To link to the case.claim object"), - 'state' : fields.selection( + 'state': fields.selection( [('draft', 'Draft'), ('refused', 'Refused'), ('confirmed', 'Confirmed, waiting for product'), @@ -160,8 +162,8 @@ class claim_line(orm.Model): '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 " + 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( @@ -245,10 +247,11 @@ class claim_line(orm.Model): warning = _(self.WARRANT_COMMENT['expired']) else: warning = _(self.WARRANT_COMMENT['valid']) - self.write(cr, uid, ids, - {'guarantee_limit': limit.strftime(DEFAULT_SERVER_DATE_FORMAT), - 'warning': warning}, - context=context) + self.write( + cr, uid, ids, + {'guarantee_limit': limit.strftime(DEFAULT_SERVER_DATE_FORMAT), + 'warning': warning}, + context=context) return True def get_destination_location(self, cr, uid, product_id, @@ -270,13 +273,14 @@ class claim_line(orm.Model): return location_dest_id # Method to calculate warranty return address - def set_warranty_return_address(self, cr, uid, ids, claim_line, context=None): + def set_warranty_return_address(self, cr, uid, ids, claim_line, + context=None): """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 case here: - - company or other: return to company partner or crm_return_address_id - if specified + - company or other: return to company partner or + crm_return_address_id if specified - supplier: return to the supplier address """ @@ -318,7 +322,8 @@ class claim_line(orm.Model): return True -#TODO add the option to split the claim_line in order to manage the same product separately +#TODO add the option to split the claim_line in order to manage the same +# product separately class crm_claim(orm.Model): _inherit = 'crm.claim' @@ -344,12 +349,14 @@ class crm_claim(orm.Model): def name_get(self, cr, uid, ids, context=None): res = [] for claim in self.browse(cr, uid, ids, context=context): - res.append((claim.id, '[' + claim.number + '] ' + claim.name)) + number = claim.number and str(claim.number) or '' + res.append((claim.id, '[' + number + '] ' + claim.name)) return res def create(self, cr, uid, vals, context=None): if ('number' not in vals) or (vals.get('number') == '/'): - vals['number'] = self._get_sequence_number(cr, uid, context=context) + vals['number'] = self._get_sequence_number(cr, uid, + context=context) new_id = super(crm_claim, self).create(cr, uid, vals, context=context) return new_id @@ -387,11 +394,16 @@ class crm_claim(orm.Model): 'planned_cost': fields.float('Expected cost'), 'real_revenue': fields.float('Real revenue'), 'real_cost': fields.float('Real cost'), - 'invoice_ids': fields.one2many('account.invoice', 'claim_id', 'Refunds'), + 'invoice_ids': fields.one2many( + 'account.invoice', 'claim_id', 'Refunds'), 'picking_ids': fields.one2many('stock.picking', 'claim_id', 'RMA'), 'invoice_id': fields.many2one( 'account.invoice', string='Invoice', help='Related original Cusotmer invoice'), + 'delivery_address_id': fields.many2one( + 'res.partner', string='Partner delivery address', + help="This address will be used to deliver repaired or replacement" + "products."), 'warehouse_id': fields.many2one( 'stock.warehouse', string='Warehouse', required=True), @@ -408,11 +420,14 @@ class crm_claim(orm.Model): 'Number/Reference must be unique per Company!'), ] - def onchange_partner_address_id(self, cr, uid, ids, add, email=False, context=None): - res = super(crm_claim, self).onchange_partner_address_id( - cr, uid, ids, add, email=email) + def onchange_partner_address_id(self, cr, uid, ids, add, email=False, + context=None): + res = super(crm_claim, self + ).onchange_partner_address_id(cr, uid, ids, add, + email=email) if add: - if not res['value']['email_from'] or not res['value']['partner_phone']: + if (not res['value']['email_from'] + or not res['value']['partner_phone']): partner_obj = self.pool.get('res.partner') address = partner_obj.browse(cr, uid, add, context=context) for other_add in address.partner_id.address: @@ -422,16 +437,20 @@ class crm_claim(orm.Model): res['value']['partner_phone'] = other_add.phone return res - def onchange_invoice_id(self, cr, uid, ids, invoice_id, warehouse_id, context=None): + def onchange_invoice_id(self, cr, uid, ids, invoice_id, warehouse_id, + context=None): invoice_line_obj = self.pool.get('account.invoice.line') + invoice_obj = self.pool.get('account.invoice') claim_line_obj = self.pool.get('claim.line') invoice_line_ids = invoice_line_obj.search( cr, uid, [('invoice_id', '=', invoice_id)], context=context) claim_lines = [] + value = {} if not warehouse_id: - warehouse_id = self._get_default_warehouse(cr, uid, context=context) + warehouse_id = self._get_default_warehouse(cr, uid, + context=context) invoice_lines = invoice_line_obj.browse(cr, uid, invoice_line_ids, context=context) for invoice_line in invoice_lines: @@ -448,4 +467,38 @@ class crm_claim(orm.Model): 'location_dest_id': location_dest_id, 'state': 'draft', }) - return {'value': {'claim_line_ids': claim_lines}} + value = {'claim_line_ids': claim_lines} + delivery_address_id = False + if invoice_id: + invoice = invoice_obj.browse(cr, uid, invoice_id, context=context) + delivery_address_id = invoice.partner_id.id + value['delivery_address_id'] = delivery_address_id + + return {'value': value} + + def message_get_reply_to(self, cr, uid, ids, context=None): + """ Override to get the reply_to of the parent project. """ + return [claim.section_id.message_get_reply_to()[0] + if claim.section_id else False + for claim in self.browse(cr, SUPERUSER_ID, ids, + context=context)] + + def message_get_suggested_recipients(self, cr, uid, ids, context=None): + recipients = super(crm_claim, self + ).message_get_suggested_recipients(cr, uid, ids, + context=context) + try: + for claim in self.browse(cr, uid, ids, context=context): + if claim.partner_id: + self._message_add_suggested_recipient( + cr, uid, recipients, claim, + partner=claim.partner_id, reason=_('Customer')) + elif claim.email_from: + self._message_add_suggested_recipient( + cr, uid, recipients, claim, + email=claim.email_from, reason=_('Customer Email')) + except (osv.except_osv, orm.except_orm): + # no read access rights -> just ignore suggested recipients + # because this imply modifying followers + pass + return recipients diff --git a/crm_claim_rma/crm_claim_rma_view.xml b/crm_claim_rma/crm_claim_rma_view.xml index 65d686b8..4948c449 100644 --- a/crm_claim_rma/crm_claim_rma_view.xml +++ b/crm_claim_rma/crm_claim_rma_view.xml @@ -166,6 +166,7 @@ + @@ -246,15 +247,15 @@ diff --git a/crm_claim_rma/i18n/crm_claim_rma.pot b/crm_claim_rma/i18n/crm_claim_rma.pot index daff7482..2a2d31af 100644 --- a/crm_claim_rma/i18n/crm_claim_rma.pot +++ b/crm_claim_rma/i18n/crm_claim_rma.pot @@ -854,7 +854,7 @@ msgstr "" #. module: crm_claim_rma #: model:ir.actions.act_window,name:crm_claim_rma.act_crm_claim_rma_picking_in -msgid "Incomming Shipment and Returns" +msgid "Incoming Shipment and Returns" msgstr "" #. module: crm_claim_rma diff --git a/crm_claim_rma/res_partner_view.xml b/crm_claim_rma/res_partner_view.xml new file mode 100644 index 00000000..ea4ac7b3 --- /dev/null +++ b/crm_claim_rma/res_partner_view.xml @@ -0,0 +1,25 @@ + + + + + + res.partner.contact.tree + res.partner + + + + + + + + + + + + + + + + + + diff --git a/crm_claim_rma/stock.py b/crm_claim_rma/stock.py index a502f8ef..ca8ab35e 100644 --- a/crm_claim_rma/stock.py +++ b/crm_claim_rma/stock.py @@ -54,7 +54,7 @@ class stock_picking_out(orm.Model): } -class stock_picking_out(orm.Model): +class stock_picking_in(orm.Model): _inherit = "stock.picking.in" @@ -72,7 +72,8 @@ class stock_move(orm.Model): _inherit = "stock.move" def create(self, cr, uid, vals, context=None): - move_id = super(stock_move, self).create(cr, uid, vals, context=context) + move_id = super(stock_move, self + ).create(cr, uid, vals, context=context) if vals.get('picking_id'): picking_obj = self.pool.get('stock.picking') picking = picking_obj.browse(cr, uid, vals['picking_id'], diff --git a/crm_claim_rma/tests/__init__.py b/crm_claim_rma/tests/__init__.py new file mode 100644 index 00000000..ffaaabfb --- /dev/null +++ b/crm_claim_rma/tests/__init__.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- +############################################################################## +# +# Author: Yannick Vaucher +# Copyright 2014 Camptocamp SA +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +############################################################################## +from . import test_lp_1282584 diff --git a/crm_claim_rma/tests/test_lp_1282584.py b/crm_claim_rma/tests/test_lp_1282584.py new file mode 100644 index 00000000..c3cd36ff --- /dev/null +++ b/crm_claim_rma/tests/test_lp_1282584.py @@ -0,0 +1,108 @@ +# -*- coding: utf-8 -*- +############################################################################## +# +# Author: Yannick Vaucher +# Copyright 2014 Camptocamp SA +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +############################################################################## +from openerp.tests import common + + +class test_lp_1282584(common.TransactionCase): + """ Test wizard open the right type of view + + The wizard can generate picking.in and picking.out + Let's ensure it open the right view for each picking type + """ + + def setUp(self): + super(test_lp_1282584, self).setUp() + cr, uid = self.cr, self.uid + + self.WizardMakePicking = self.registry('claim_make_picking.wizard') + ClaimLine = self.registry('claim.line') + Claim = self.registry('crm.claim') + + self.product_id = self.ref('product.product_product_4') + + self.partner_id = self.ref('base.res_partner_12') + + # Create the claim with a claim line + self.claim_id = Claim.create( + cr, uid, + { + 'name': 'TEST CLAIM', + 'number': 'TEST CLAIM', + 'claim_type': 'customer', + 'delivery_address_id': self.partner_id, + }) + + claim = Claim.browse(cr, uid, self.claim_id) + self.warehouse_id = claim.warehouse_id.id + self.claim_line_id = ClaimLine.create( + cr, uid, + { + 'name': 'TEST CLAIM LINE', + 'claim_origine': 'none', + 'product_id': self.product_id, + 'claim_id': self.claim_id, + 'location_dest_id': claim.warehouse_id.lot_stock_id.id + }) + + def test_00(self): + """Test wizard opened view model for a new product return + + """ + cr, uid = self.cr, self.uid + wiz_context = { + 'active_id': self.claim_id, + 'partner_id': self.partner_id, + 'warehouse_id': self.warehouse_id, + 'picking_type': 'in', + } + wizard_id = self.WizardMakePicking.create(cr, uid, { + }, context=wiz_context) + + res = self.WizardMakePicking.action_create_picking( + cr, uid, [wizard_id], context=wiz_context) + self.assertEquals(res.get('res_model'), 'stock.picking.in', "Wrong model defined") + + def test_01(self): + """Test wizard opened view model for a new delivery + + """ + + cr, uid = self.cr, self.uid + + WizardChangeProductQty = self.registry('stock.change.product.qty') + wiz_context = {'active_id': self.product_id} + wizard_chg_qty_id = WizardChangeProductQty.create(cr, uid, { + 'product_id': self.product_id, + 'new_quantity': 12}) + WizardChangeProductQty.change_product_qty(cr, uid, [wizard_chg_qty_id], context=wiz_context) + + wiz_context = { + 'active_id': self.claim_id, + 'partner_id': self.partner_id, + 'warehouse_id': self.warehouse_id, + 'picking_type': 'out', + } + wizard_id = self.WizardMakePicking.create(cr, uid, { + }, context=wiz_context) + + res = self.WizardMakePicking.action_create_picking( + cr, uid, [wizard_id], context=wiz_context) + self.assertEquals(res.get('res_model'), 'stock.picking.out', "Wrong model defined") diff --git a/crm_claim_rma/wizard/account_invoice_refund.py b/crm_claim_rma/wizard/account_invoice_refund.py index 3248089a..568e3774 100644 --- a/crm_claim_rma/wizard/account_invoice_refund.py +++ b/crm_claim_rma/wizard/account_invoice_refund.py @@ -29,7 +29,7 @@ class account_invoice_refund(orm.TransientModel): def compute_refund(self, cr, uid, ids, mode='refund', context=None): if context is None: - context={} + context = {} if context.get('invoice_ids'): context['active_ids'] = context.get('invoice_ids') return super(account_invoice_refund, self).compute_refund( diff --git a/crm_claim_rma/wizard/claim_make_picking.py b/crm_claim_rma/wizard/claim_make_picking.py index 6c556466..a8bedf2b 100644 --- a/crm_claim_rma/wizard/claim_make_picking.py +++ b/crm_claim_rma/wizard/claim_make_picking.py @@ -20,7 +20,7 @@ # along with this program. If not, see . # ############################################################################## -from openerp.osv import fields, orm, osv +from openerp.osv import fields, orm from openerp.tools import DEFAULT_SERVER_DATETIME_FORMAT from openerp import netsvc from openerp.tools.translate import _ @@ -51,7 +51,8 @@ class claim_make_picking(orm.TransientModel): } def _get_claim_lines(self, cr, uid, context): - #TODO use custom states to show buttons of this wizard or not instead of raise an error + #TODO use custom states to show buttons of this wizard or not instead + # of raise an error if context is None: context = {} line_obj = self.pool.get('claim.line') @@ -89,7 +90,7 @@ class claim_make_picking(orm.TransientModel): loc_id = self.pool.get('res.partner').read( cr, uid, context['partner_id'], ['property_stock_customer'], - context=context)['property_stock_customer'] + context=context)['property_stock_customer'][0] return loc_id def _get_common_dest_location_from_line(self, cr, uid, line_ids, context): @@ -112,7 +113,8 @@ class claim_make_picking(orm.TransientModel): line_obj = self.pool.get('claim.line') line_partner = [] for line in line_obj.browse(cr, uid, line_ids, context=context): - if (line.warranty_return_partner and line.warranty_return_partner.id + if (line.warranty_return_partner + and line.warranty_return_partner.id not in line_partner): line_partner.append(line.warranty_return_partner.id) if len(line_partner) == 1: @@ -123,8 +125,8 @@ class claim_make_picking(orm.TransientModel): def _get_dest_loc(self, cr, uid, context): """Return the location_id to use as destination. If it's an outoing shippment: take the customer stock property - If it's an incomming shippment take the location_dest_id common to all lines, or - if different, return None.""" + If it's an incoming shippment take the location_dest_id common to all + lines, or if different, return None.""" if context is None: context = {} loc_id = False @@ -162,27 +164,23 @@ class claim_make_picking(orm.TransientModel): write_field = 'move_out_id' note = 'RMA picking out' view_xml_id = 'stock_picking_form' - view_name = 'stock.picking.form' else: p_type = 'in' - view_xml_id = 'stock_picking_form' - view_name = 'stock.picking.form' write_field = 'move_in_id' if context.get('picking_type'): note = 'RMA picking ' + str(context.get('picking_type')) name = note + model = 'stock.picking.' + p_type view_id = view_obj.search(cr, uid, - [('xml_id', '=', view_xml_id), - ('model', '=', 'stock.picking'), + [('model', '=', model), ('type', '=', 'form'), - ('name', '=', view_name) ], context=context)[0] wizard = self.browse(cr, uid, ids[0], context=context) claim = self.pool.get('crm.claim').browse(cr, uid, context['active_id'], context=context) - partner_id = claim.partner_id.id + partner_id = claim.delivery_address_id.id line_ids = [x.id for x in wizard.claim_line_ids] # In case of product return, we don't allow one picking for various # product if location are different @@ -220,7 +218,7 @@ class claim_make_picking(orm.TransientModel): 'location_dest_id': wizard.claim_line_dest_location.id, 'note': note, 'claim_id': claim.id, - }, + }, context=context) # Create picking lines for wizard_claim_line in wizard.claim_line_ids: @@ -261,7 +259,7 @@ class claim_make_picking(orm.TransientModel): 'view_mode': 'form', 'view_id': view_id, 'domain': domain, - 'res_model': 'stock.picking', + 'res_model': model, 'res_id': picking_id, 'type': 'ir.actions.act_window', }