From e4dc50bf58d508d95b4ce9909af7e0ddfa2e46cf Mon Sep 17 00:00:00 2001 From: qdp-odoo Date: Mon, 31 Aug 2015 16:57:32 +0200 Subject: [PATCH] [IMP] pricelists improvements. Was PR #8228 --- .../account/models/account_analytic_line.py | 31 +- addons/account/models/chart_template.py | 2 +- .../hr_expense/security/ir.model.access.csv | 1 - addons/mrp/security/ir.model.access.csv | 4 - addons/mrp_repair/mrp_repair.py | 4 +- .../mrp_repair/security/ir.model.access.csv | 2 - addons/point_of_sale/point_of_sale.py | 2 +- addons/product/partner.py | 1 - addons/product/partner_view.xml | 2 +- addons/product/pricelist.py | 361 +++++------------- addons/product/pricelist_view.xml | 249 ++++-------- addons/product/product.py | 91 +++-- addons/product/product_data.xml | 46 +-- addons/product/product_demo.xml | 10 + addons/product/product_pricelist_demo.yml | 86 ++--- addons/product/product_view.xml | 69 +++- addons/product/security/ir.model.access.csv | 5 - addons/product/security/product_security.xml | 16 +- addons/product/test/product_pricelist.yml | 6 +- addons/product/tests/test_pricelist.py | 42 +- .../product_visible_discount.py | 31 +- .../product_visible_discount_view.xml | 9 +- addons/purchase/partner.py | 13 +- addons/purchase/partner_view.xml | 9 +- addons/purchase/purchase.py | 139 +++---- addons/purchase/purchase_data.xml | 25 -- addons/purchase/purchase_view.xml | 57 +-- addons/purchase/report/purchase_report.py | 4 +- addons/purchase/res_config.py | 7 - addons/purchase/res_config_view.xml | 1 - addons/purchase/security/ir.model.access.csv | 6 - addons/purchase/stock.py | 35 +- addons/purchase/test/anglo_saxon.yml | 1 - addons/purchase/test/anglo_saxon_avg_fifo.yml | 1 - addons/purchase/test/average_price.yml | 3 - addons/purchase/test/costmethodchange.yml | 3 - addons/purchase/test/fifo_price.yml | 45 +-- addons/purchase/test/fifo_returns.yml | 2 - addons/purchase/test/process/merge_order.yml | 1 - .../tests/test_onchange_product_id.py | 2 +- .../tests/test_purchase_to_invoice.py | 3 +- .../purchase_requisition.py | 7 +- .../purchase_requisition_demo.xml | 2 - addons/sale/res_config.py | 25 +- addons/sale/res_config_view.xml | 10 +- addons/sale/sale_view.xml | 2 +- addons/sale/security/ir.model.access.csv | 4 - addons/sales_team/sales_team.py | 2 +- addons/stock/security/ir.model.access.csv | 4 - addons/stock/stock.py | 1 - addons/stock_dropshipping/test/lifo_price.yml | 8 +- addons/website_sale/controllers/main.py | 7 +- addons/website_sale/data/demo.xml | 35 +- addons/website_sale/models/product.py | 2 +- .../website_sale/security/ir.model.access.csv | 3 - addons/website_sale/views/views.xml | 2 +- .../website_sale_options/controllers/main.py | 2 +- 57 files changed, 544 insertions(+), 999 deletions(-) diff --git a/addons/account/models/account_analytic_line.py b/addons/account/models/account_analytic_line.py index a2f06c778d7..68ac8cbff8a 100644 --- a/addons/account/models/account_analytic_line.py +++ b/addons/account/models/account_analytic_line.py @@ -1,7 +1,6 @@ # -*- coding: utf-8 -*- from openerp import api, fields, models, _ -from openerp.exceptions import UserError class AccountAnalyticLine(models.Model): @@ -21,17 +20,17 @@ class AccountAnalyticLine(models.Model): partner_id = fields.Many2one('res.partner', related='account_id.partner_id', string='Partner', store=True) @api.v8 - @api.onchange('product_id', 'product_uom_id') + @api.onchange('product_id', 'product_uom_id', 'unit_amount', 'currency_id') def on_change_unit_amount(self): - product_price_type_obj = self.env['product.price.type'] + if not self.product_id: + return {} + result = 0.0 - unit = False - if self.product_id: - unit = self.product_uom_id.id - if not self.product_uom_id or self.product_id.uom_id.category_id.id != self.product_uom_id.category_id.id: - unit = self.product_id.uom_id.id - account = self.product_id.property_account_income_id.id or self.product_id.categ_id.property_account_income_categ_id.id - if not account: account = False + prod_accounts = self.product_id.product_tmpl_id._get_product_accounts() + unit = self.product_uom_id.id + account = prod_accounts['expense'] + if not unit or self.product_id.uom_po_id.category_id.id != unit.category_id.id: + unit = self.product_id.uom_po_id.id ctx = dict(self._context or {}) if unit: @@ -39,16 +38,10 @@ class AccountAnalyticLine(models.Model): # to return a default price for those units ctx['uom'] = unit - pricetype = False - amount_unit = 0.0 - if self.product_id: - # Compute based on pricetype - pricetype = product_price_type_obj.search([('field', '=', 'list_price')], limit=1) - amount_unit = self.product_id.with_context(ctx).price_get(pricetype.field)[self.product_id.id] + # Compute based on pricetype + amount_unit = self.product_id.with_context(ctx).price_get('standard_price')[self.product_id.id] amount = amount_unit * self.unit_amount or 0.0 - result = round(amount, self.currency_id.decimal_places) - if pricetype and pricetype.field != 'list_price': - result *= -1 + result = round(amount, self.currency_id.decimal_places) * -1 self.amount = result self.general_account_id = account self.product_uom_id = unit diff --git a/addons/account/models/chart_template.py b/addons/account/models/chart_template.py index 5501ccf0739..32740b99ddc 100644 --- a/addons/account/models/chart_template.py +++ b/addons/account/models/chart_template.py @@ -703,7 +703,7 @@ class WizardMultiChartsAccounts(models.TransientModel): # When we install the CoA of first company, set the currency to price types and pricelists if company.id == 1: - for reference in ['product.list_price', 'product.standard_price', 'product.list0', 'purchase.list0']: + for reference in ['product.list_price', 'product.standard_price', 'product.list0']: try: tmp2 = self.env.ref(reference).write({'currency_id': self.currency_id.id}) except ValueError: diff --git a/addons/hr_expense/security/ir.model.access.csv b/addons/hr_expense/security/ir.model.access.csv index bfc865a5243..3b59343b112 100644 --- a/addons/hr_expense/security/ir.model.access.csv +++ b/addons/hr_expense/security/ir.model.access.csv @@ -7,7 +7,6 @@ access_hr_expense_report_manager,hr.expense.report.manager,model_hr_expense_repo access_product_product_hr_expense_user,product.product.hr.expense.user,product.model_product_product,base.group_hr_user,1,1,1,1 access_product_template_hr_expense_user,product.template.hr.expense.user,product.model_product_template,base.group_hr_user,1,1,1,1 access_product_uom_hr_expense_user,product.uom.hr.expense.user,product.model_product_uom,base.group_hr_user,1,1,1,1 -access_product_price_type_user,product.price.type.user,product.model_product_price_type,base.group_hr_user,1,1,1,1 access_account_journal_user,account.journal.user,account.model_account_journal,base.group_hr_user,1,1,1,1 access_account_journal_employee,account.journal.employee,account.model_account_journal,base.group_user,1,0,0,0 access_account_invoice_user,account.invoice.user,account.model_account_invoice,base.group_hr_user,1,1,1,1 diff --git a/addons/mrp/security/ir.model.access.csv b/addons/mrp/security/ir.model.access.csv index a270620491a..89b966f4a08 100644 --- a/addons/mrp/security/ir.model.access.csv +++ b/addons/mrp/security/ir.model.access.csv @@ -62,13 +62,9 @@ access_product_category_mrp_manager,product.category mrp_manager,product.model_p access_product_template_mrp_manager,product.template mrp_manager,product.model_product_template,mrp.group_mrp_manager,1,1,1,1 access_product_product_mrp_manager,product.product mrp_manager,product.model_product_product,mrp.group_mrp_manager,1,1,1,1 access_product_packaging_mrp_manager,product.packaging mrp_manager,product.model_product_packaging,mrp.group_mrp_manager,1,1,1,1 -access_pricelist_partnerinfo_mrp_manager,pricelist.partnerinfo mrp_manager,product.model_pricelist_partnerinfo,mrp.group_mrp_manager,1,1,1,1 -access_product_price_type_mrp_manager,product.price.type mrp_manager,product.model_product_price_type,mrp.group_mrp_manager,1,1,1,1 -access_product_pricelist_type_mrp_manager,product.pricelist.type mrp_manager,product.model_product_pricelist_type,mrp.group_mrp_manager,1,1,1,1 access_product_pricelist_mrp_manager,product.pricelist mrp_manager,product.model_product_pricelist,mrp.group_mrp_manager,1,1,1,1 access_ir_property_group_product_mrp_manager,ir_property group_product_mrp_manager,base.model_ir_property,mrp.group_mrp_manager,1,1,1,1 access_product_group_res_partner_mrp_manager,res_partner group_mrp_manager,base.model_res_partner,mrp.group_mrp_manager,1,1,1,0 -access_product_pricelist_version_mrp_manager,product.pricelist.version mrp_manager,product.model_product_pricelist_version,mrp.group_mrp_manager,1,1,1,1 access_product_pricelist_item_mrp_manager,product.pricelist.item mrp_manager,product.model_product_pricelist_item,mrp.group_mrp_manager,1,1,1,1 access_resource_calendar_manufacturinguser,resource.calendar manufacturing.user,resource.model_resource_calendar,mrp.group_mrp_user,1,0,0,0 access_account_journal_mrp_manager,account.journal mrp manager,account.model_account_journal,mrp.group_mrp_manager,1,0,0,0 diff --git a/addons/mrp_repair/mrp_repair.py b/addons/mrp_repair/mrp_repair.py index 1f871a6a366..9fca802ae88 100644 --- a/addons/mrp_repair/mrp_repair.py +++ b/addons/mrp_repair/mrp_repair.py @@ -173,7 +173,7 @@ class mrp_repair(osv.osv): 'name': lambda obj, cr, uid, context: obj.pool.get('ir.sequence').next_by_code(cr, uid, 'mrp.repair'), 'invoice_method': lambda *a: 'none', 'company_id': lambda self, cr, uid, context: self.pool.get('res.company')._company_default_get(cr, uid, 'mrp.repair', context=context), - 'pricelist_id': lambda self, cr, uid, context: self.pool.get('product.pricelist').search(cr, uid, [('type', '=', 'sale')])[0], + 'pricelist_id': lambda self, cr, uid, context: self.pool['product.pricelist'].search(cr, uid, [], limit=1)[0], 'product_qty': 1.0, 'location_id': _default_stock_location, } @@ -229,7 +229,7 @@ class mrp_repair(osv.osv): return {'value': { 'address_id': False, 'partner_invoice_id': False, - 'pricelist_id': pricelist_obj.search(cr, uid, [('type', '=', 'sale')])[0] + 'pricelist_id': pricelist_obj.search(cr, uid, [], limit=1)[0] } } addr = part_obj.address_get(cr, uid, [part], ['delivery', 'invoice', 'contact']) diff --git a/addons/mrp_repair/security/ir.model.access.csv b/addons/mrp_repair/security/ir.model.access.csv index 95f2462909a..89a7d680ae0 100644 --- a/addons/mrp_repair/security/ir.model.access.csv +++ b/addons/mrp_repair/security/ir.model.access.csv @@ -13,8 +13,6 @@ access_stock_production_lot_user,stock.production.lot user,stock.model_stock_pro access_stock_production_lot_manager,stock.production.lot manager,stock.model_stock_production_lot,stock.group_stock_manager,1,0,0,0 access_mrp_repair_line_user,mrp.repair.line user,model_mrp_repair_line,stock.group_stock_user,1,1,1,1 access_mrp_repair_line_manager,mrp.repair.line manager,model_mrp_repair_line,stock.group_stock_manager,1,0,0,0 -access_product_price_type_manager,product.price.type manager,product.model_product_price_type,stock.group_stock_manager,1,0,0,0 -access_product_price_type_user,product.price.type,product.model_product_price_type,stock.group_stock_user,1,1,1,1 access_account_tax_user,account.tax,account.model_account_tax,stock.group_stock_user,1,1,1,1 access_account_tax_manager,account.tax manager,account.model_account_tax,stock.group_stock_manager,1,0,0,0 access_mrp_repair_fee_user_mrp,MRP Repair Fee user mrp,model_mrp_repair_fee,stock.group_stock_user,1,1,1,1 diff --git a/addons/point_of_sale/point_of_sale.py b/addons/point_of_sale/point_of_sale.py index 228d136d2c8..bbc5d2497e5 100644 --- a/addons/point_of_sale/point_of_sale.py +++ b/addons/point_of_sale/point_of_sale.py @@ -179,7 +179,7 @@ class pos_config(osv.osv): return res and res[0] or False def _default_pricelist(self, cr, uid, context=None): - res = self.pool.get('product.pricelist').search(cr, uid, [('type', '=', 'sale')], limit=1, context=context) + res = self.pool.get('product.pricelist').search(cr, uid, [], limit=1, context=context) return res and res[0] or False def _get_default_location(self, cr, uid, context=None): diff --git a/addons/product/partner.py b/addons/product/partner.py index 08c5354afc3..35db117460c 100644 --- a/addons/product/partner.py +++ b/addons/product/partner.py @@ -11,7 +11,6 @@ class res_partner(osv.osv): 'property_product_pricelist': fields.property( type='many2one', relation='product.pricelist', - domain=[('type','=','sale')], string="Sale Pricelist", help="This pricelist will be used, instead of the default one, for sales to the current partner"), } diff --git a/addons/product/partner_view.xml b/addons/product/partner_view.xml index 813e8c9a59a..61a4d11186b 100644 --- a/addons/product/partner_view.xml +++ b/addons/product/partner_view.xml @@ -5,7 +5,7 @@ res.partner.product.property.form.inherit res.partner - + diff --git a/addons/product/pricelist.py b/addons/product/pricelist.py index 3f11cde9948..a70d8bc7ea3 100644 --- a/addons/product/pricelist.py +++ b/addons/product/pricelist.py @@ -5,79 +5,18 @@ from itertools import chain import time from openerp import tools +from openerp.tools import DEFAULT_SERVER_DATE_FORMAT from openerp.osv import fields, osv from openerp.tools.translate import _ import openerp.addons.decimal_precision as dp from openerp.exceptions import UserError - -class price_type(osv.osv): - """ - The price type is used to points which field in the product form - is a price and in which currency is this price expressed. - When a field is a price, you can use it in pricelists to base - sale and purchase prices based on some fields of the product. - """ - def _price_field_get(self, cr, uid, context=None): - mf = self.pool.get('ir.model.fields') - ids = mf.search(cr, uid, [('model','in', (('product.product'),('product.template'))), ('ttype','=','float')], context=context) - res = [] - for field in mf.browse(cr, uid, ids, context=context): - if not (field.name, field.field_description) in res: - res.append((field.name, field.field_description)) - return res - - def _get_field_currency(self, cr, uid, fname, ctx): - ids = self.search(cr, uid, [('field','=',fname)], context=ctx) - return self.browse(cr, uid, ids, context=ctx)[0].currency_id - - def _get_currency(self, cr, uid, ctx): - comp = self.pool.get('res.users').browse(cr,uid,uid).company_id - if not comp: - comp_id = self.pool.get('res.company').search(cr, uid, [])[0] - comp = self.pool.get('res.company').browse(cr, uid, comp_id) - return comp.currency_id.id - - _name = "product.price.type" - _description = "Price Type" - _columns = { - "name" : fields.char("Price Name", required=True, translate=True, help="Name of this kind of price."), - "active" : fields.boolean("Active"), - "field" : fields.selection(_price_field_get, "Product Field", size=32, required=True, help="Associated field in the product form."), - "currency_id" : fields.many2one('res.currency', "Currency", required=True, help="The currency the field is expressed in."), - } - _defaults = { - "active": lambda *args: True, - "currency_id": _get_currency - } - - #---------------------------------------------------------- # Price lists #---------------------------------------------------------- -class product_pricelist_type(osv.osv): - _name = "product.pricelist.type" - _description = "Pricelist Type" - _columns = { - 'name': fields.char('Name', required=True, translate=True), - 'key': fields.char('Key', required=True, help="Used in the code to select specific prices based on the context. Keep unchanged."), - } - - class product_pricelist(osv.osv): - def _pricelist_type_get(self, cr, uid, context=None): - pricelist_type_obj = self.pool.get('product.pricelist.type') - pricelist_type_ids = pricelist_type_obj.search(cr, uid, [], order='name') - pricelist_types = pricelist_type_obj.read(cr, uid, pricelist_type_ids, ['key','name'], context=context) - - res = [] - - for type in pricelist_types: - res.append((type['key'],type['name'])) - - return res _name = "product.pricelist" _description = "Pricelist" @@ -85,8 +24,7 @@ class product_pricelist(osv.osv): _columns = { 'name': fields.char('Pricelist Name', required=True, translate=True), 'active': fields.boolean('Active', help="If unchecked, it will allow you to hide the pricelist without removing it."), - 'type': fields.selection(_pricelist_type_get, 'Pricelist Type', required=True), - 'version_id': fields.one2many('product.pricelist.version', 'pricelist_id', 'Pricelist Versions', copy=True), + 'item_ids': fields.one2many('product.pricelist.item', 'pricelist_id', 'Pricelist Items', copy=True), 'currency_id': fields.many2one('res.currency', 'Currency', required=True), 'company_id': fields.many2one('res.company', 'Company'), } @@ -139,7 +77,6 @@ class product_pricelist(osv.osv): return super(product_pricelist, self).name_search( cr, uid, name, args, operator=operator, context=context, limit=limit) - def _get_currency(self, cr, uid, ctx): comp = self.pool.get('res.users').browse(cr, uid, uid).company_id if not comp: @@ -152,9 +89,6 @@ class product_pricelist(osv.osv): "currency_id": _get_currency } - def price_get_multi(self, cr, uid, ids, products_by_qty_by_partner, context=None): - return dict((key, dict((key, price[0]) for key, price in value.items())) for key, value in self.price_rule_get_multi(cr, uid, ids, products_by_qty_by_partner, context=context).items()) - def price_rule_get_multi(self, cr, uid, ids, products_by_qty_by_partner, context=None): """multi products 'price_get'. @param ids: @@ -169,7 +103,7 @@ class product_pricelist(osv.osv): results = {} for pricelist in self.browse(cr, uid, ids, context=context): subres = self._price_rule_get_multi(cr, uid, pricelist, products_by_qty_by_partner, context=context) - for product_id,price in subres.items(): + for product_id, price in subres.items(): results.setdefault(product_id, {}) results[product_id][pricelist.id] = price return results @@ -179,25 +113,13 @@ class product_pricelist(osv.osv): def _price_rule_get_multi(self, cr, uid, pricelist, products_by_qty_by_partner, context=None): context = context or {} - date = context.get('date') or time.strftime('%Y-%m-%d') - date = date[0:10] - + date = context.get('date') and context['date'][0:10] or time.strftime(DEFAULT_SERVER_DATE_FORMAT) products = map(lambda x: x[0], products_by_qty_by_partner) - currency_obj = self.pool.get('res.currency') - product_obj = self.pool.get('product.template') product_uom_obj = self.pool.get('product.uom') - price_type_obj = self.pool.get('product.price.type') if not products: return {} - version = False - for v in pricelist.version_id: - if ((v.date_start is False) or (v.date_start <= date)) and ((v.date_end is False) or (v.date_end >= date)): - version = v - break - if not version: - raise UserError(_("At least one pricelist has no active version !\nPlease create or activate one.")) categ_ids = {} for p in products: categ = p.categ_id @@ -220,22 +142,20 @@ class product_pricelist(osv.osv): cr.execute( 'SELECT i.id ' 'FROM product_pricelist_item AS i ' - 'WHERE (product_tmpl_id IS NULL OR product_tmpl_id = any(%s)) ' - 'AND (product_id IS NULL OR (product_id = any(%s))) ' - 'AND ((categ_id IS NULL) OR (categ_id = any(%s))) ' - 'AND (price_version_id = %s) ' - 'ORDER BY sequence, min_quantity desc', - (prod_tmpl_ids, prod_ids, categ_ids, version.id)) - + 'WHERE (product_tmpl_id IS NULL OR product_tmpl_id = any(%s))' + 'AND (product_id IS NULL OR product_id = any(%s))' + 'AND (categ_id IS NULL OR categ_id = any(%s)) ' + 'AND (pricelist_id = %s) ' + 'AND ((i.date_start IS NULL OR i.date_start<=%s) AND (i.date_end IS NULL OR i.date_end>=%s))' + 'ORDER BY applied_on, min_quantity desc', + (prod_tmpl_ids, prod_ids, categ_ids, pricelist.id, date, date)) + item_ids = [x[0] for x in cr.fetchall()] items = self.pool.get('product.pricelist.item').browse(cr, uid, item_ids, context=context) - - price_types = {} - results = {} for product, qty, partner in products_by_qty_by_partner: results[product.id] = 0.0 - rule_id = False + suitable_rule = False # Final unit price is computed according to `qty` in the `qty_uom_id` UoM. # An intermediary unit price may be computed according to a different UoM, in @@ -252,9 +172,10 @@ class product_pricelist(osv.osv): # Ignored - incompatible UoM in context, use default product UoM pass - price_type = 'standard_price' if pricelist.type == 'purchase' else 'list_price' # if Public user try to access standard price from website sale, need to call _price_get. - price = product_obj._price_get(cr, uid, [product], price_type, context=context)[product.id] or False + price = self.pool['product.template']._price_get(cr, uid, [product], 'list_price', context=context)[product.id] + + price_uom_id = qty_uom_id for rule in items: if rule.min_quantity and qty_in_product_uom < rule.min_quantity: continue @@ -271,68 +192,50 @@ class product_pricelist(osv.osv): if rule.product_id and product.id != rule.product_id.id: continue - if rule.base == -1: - if rule.base_pricelist_id: - price_tmp = self._price_get_multi(cr, uid, - rule.base_pricelist_id, [(product, - qty, partner)], context=context)[product.id] - ptype_src = rule.base_pricelist_id.currency_id.id - price_uom_id = qty_uom_id - price = currency_obj.compute(cr, uid, - ptype_src, pricelist.currency_id.id, - price_tmp, round=False, - context=context) + if rule.base == 'pricelist' and rule.base_pricelist_id: + price_tmp = self._price_get_multi(cr, uid, rule.base_pricelist_id, [(product, qty, partner)], context=context)[product.id] + ptype_src = rule.base_pricelist_id.currency_id.id + price = self.pool['res.currency'].compute(cr, uid, ptype_src, pricelist.currency_id.id, price_tmp, round=False, context=context) else: - if rule.base not in price_types: - price_types[rule.base] = price_type_obj.browse(cr, uid, int(rule.base)) - price_type = price_types[rule.base] - + # if base option is public price take sale price else cost price of product # price_get returns the price in the context UoM, i.e. qty_uom_id - price_uom_id = qty_uom_id - price = currency_obj.compute( - cr, uid, - price_type.currency_id.id, pricelist.currency_id.id, - product_obj._price_get(cr, uid, [product], price_type.field, context=context)[product.id], - round=False, context=context) - for seller in product.seller_ids: - partner = partner.id if partner and not isinstance(partner, int) else partner - if seller.name.id == partner: - qty_in_seller = qty - seller_uom = seller.product_uom and seller.product_uom.id or False - if qty_uom_id != seller_uom: - qty_in_seller = product_uom_obj._compute_qty(cr, uid, qty_uom_id, qty, to_uom_id=seller_uom) - for line in seller.pricelist_ids: - if line.min_quantity <= qty_in_seller: - price = line.price + price = self.pool['product.template']._price_get(cr, uid, [product], rule.base, context=context)[product.id] + + convert_to_price_uom = (lambda price: product_uom_obj._compute_price( + cr, uid, product.uom_id.id, + price, price_uom_id)) if price is not False: - price_limit = price - price = price * (1.0+(rule.price_discount or 0.0)) - if rule.price_round: - price = tools.float_round(price, precision_rounding=rule.price_round) + if rule.compute_price == 'fixed': + price = convert_to_price_uom(rule.fixed_price) + elif rule.compute_price == 'percentage': + price = (price - (price * (rule.percent_price / 100))) or 0.0 + else: + #complete formula + price_limit = price + price = (price - (price * (rule.price_discount / 100))) or 0.0 + if rule.price_round: + price = tools.float_round(price, precision_rounding=rule.price_round) - convert_to_price_uom = (lambda price: product_uom_obj._compute_price( - cr, uid, product.uom_id.id, - price, price_uom_id)) - if rule.price_surcharge: - price_surcharge = convert_to_price_uom(rule.price_surcharge) - price += price_surcharge + if rule.price_surcharge: + price_surcharge = convert_to_price_uom(rule.price_surcharge) + price += price_surcharge - if rule.price_min_margin: - price_min_margin = convert_to_price_uom(rule.price_min_margin) - price = max(price, price_limit + price_min_margin) + if rule.price_min_margin: + price_min_margin = convert_to_price_uom(rule.price_min_margin) + price = max(price, price_limit + price_min_margin) - if rule.price_max_margin: - price_max_margin = convert_to_price_uom(rule.price_max_margin) - price = min(price, price_limit + price_max_margin) - - rule_id = rule.id + if rule.price_max_margin: + price_max_margin = convert_to_price_uom(rule.price_max_margin) + price = min(price, price_limit + price_max_margin) + suitable_rule = rule break + # Final price conversion into pricelist currency + if suitable_rule and suitable_rule.compute_price != 'fixed' and suitable_rule.base != 'pricelist': + user_company = self.pool['res.users'].browse(cr, uid, uid, context=context).company_id + price = self.pool['res.currency'].compute(cr, uid, user_company.currency_id.id, pricelist.currency_id.id, price, context=context) - # Final price conversion to target UoM - price = product_uom_obj._compute_price(cr, uid, price_uom_id, price, qty_uom_id) - - results[product.id] = (price, rule_id) + results[product.id] = (price, suitable_rule and suitable_rule.id or False) return results def price_get(self, cr, uid, ids, prod_id, qty, partner=None, context=None): @@ -344,104 +247,15 @@ class product_pricelist(osv.osv): res = res_multi[prod_id] return res - -class product_pricelist_version(osv.osv): - _name = "product.pricelist.version" - _description = "Pricelist Version" - _columns = { - 'pricelist_id': fields.many2one('product.pricelist', 'Price List', - required=True, select=True, ondelete='cascade'), - 'name': fields.char('Name', required=True, translate=True), - 'active': fields.boolean('Active', - help="When a version is duplicated it is set to non active, so that the " \ - "dates do not overlaps with original version. You should change the dates " \ - "and reactivate the pricelist"), - 'items_id': fields.one2many('product.pricelist.item', - 'price_version_id', 'Price List Items', required=True, copy=True), - 'date_start': fields.date('Start Date', help="First valid date for the version."), - 'date_end': fields.date('End Date', help="Last valid date for the version."), - 'company_id': fields.related('pricelist_id','company_id',type='many2one', - readonly=True, relation='res.company', string='Company', store=True) - } - _defaults = { - 'active': lambda *a: 1, - } - - def _check_date(self, cursor, user, ids, context=None): - for pricelist_version in self.browse(cursor, user, ids, context=context): - if not pricelist_version.active: - continue - where = [] - if pricelist_version.date_start: - where.append("((date_end>='%s') or (date_end is null))" % (pricelist_version.date_start,)) - if pricelist_version.date_end: - where.append("((date_start<='%s') or (date_start is null))" % (pricelist_version.date_end,)) - - cursor.execute('SELECT id ' \ - 'FROM product_pricelist_version ' \ - 'WHERE '+' and '.join(where) + (where and ' and ' or '')+ - 'pricelist_id = %s ' \ - 'AND active ' \ - 'AND id <> %s', ( - pricelist_version.pricelist_id.id, - pricelist_version.id)) - if cursor.fetchall(): - return False - return True - - _constraints = [ - (_check_date, 'You cannot have 2 pricelist versions that overlap!', - ['date_start', 'date_end']) - ] - - def copy(self, cr, uid, id, default=None, context=None): - # set active False to prevent overlapping active pricelist - # versions - if not default: - default = {} - default['active'] = False - return super(product_pricelist_version, self).copy(cr, uid, id, default, context=context) - class product_pricelist_item(osv.osv): - def _price_field_get(self, cr, uid, context=None): - pt = self.pool.get('product.price.type') - ids = pt.search(cr, uid, [], context=context) - result = [] - for line in pt.browse(cr, uid, ids, context=context): - result.append((line.id, line.name)) - - result.append((-1, _('Other Pricelist'))) - return result - -# Added default function to fetch the Price type Based on Pricelist type. - def _get_default_base(self, cr, uid, fields, context=None): - product_price_type_obj = self.pool.get('product.price.type') - if fields.get('type') == 'purchase': - product_price_type_ids = product_price_type_obj.search(cr, uid, [('field', '=', 'standard_price')], context=context) - elif fields.get('type') == 'sale': - product_price_type_ids = product_price_type_obj.search(cr, uid, [('field','=','list_price')], context=context) - else: - return -1 - if not product_price_type_ids: - return False - else: - pricetype = product_price_type_obj.browse(cr, uid, product_price_type_ids, context=context)[0] - return pricetype.id - _name = "product.pricelist.item" _description = "Pricelist item" - _order = "sequence, min_quantity desc" - _defaults = { - 'base': _get_default_base, - 'min_quantity': lambda *a: 0, - 'sequence': lambda *a: 5, - 'price_discount': lambda *a: 0, - } + _order = "applied_on, min_quantity desc" def _check_recursion(self, cr, uid, ids, context=None): for obj_list in self.browse(cr, uid, ids, context=context): - if obj_list.base == -1: - main_pricelist = obj_list.price_version_id.pricelist_id.id + if obj_list.base == 'pricelist': + main_pricelist = obj_list.pricelist_id.id other_pricelist = obj_list.base_pricelist_id.id if main_pricelist == other_pricelist: return False @@ -453,26 +267,48 @@ class product_pricelist_item(osv.osv): return False return True + def _get_pricelist_item_name_price(self, cr, uid, ids, fields, args, context=None): + """This function is used to set some fields used for usability purposes only (state explicitly what a rule does) + """ + res = {} + for item in self.browse(cr, uid, ids, context=context): + res[item.id] = {'name': '', 'price': ''} + if item.categ_id: + res[item.id]['name'] = _("Category: %s") % (item.categ_id.name) + elif item.product_tmpl_id: + res[item.id]['name'] = item.product_tmpl_id.name + elif item.product_id: + res[item.id]['name'] = item.product_id.display_name.replace('[%s]' % item.product_id.code, '') + else: + res[item.id]['name'] = _("All Products") + + if item.compute_price == 'fixed': + res[item.id]['price'] = ("%s %s") % (item.fixed_price, item.pricelist_id.currency_id.name) + elif item.compute_price == 'percentage': + res[item.id]['price'] = _("%s %% discount") % (item.percent_price) + else: + res[item.id]['price'] = _("%s %% discount and %s surcharge") % (abs(item.price_discount), item.price_surcharge) + return res + _columns = { - 'name': fields.char('Rule Name', help="Explicit rule name for this pricelist line."), - 'price_version_id': fields.many2one('product.pricelist.version', 'Price List Version', required=True, select=True, ondelete='cascade'), 'product_tmpl_id': fields.many2one('product.template', 'Product Template', ondelete='cascade', help="Specify a template if this rule only applies to one product template. Keep empty otherwise."), 'product_id': fields.many2one('product.product', 'Product', ondelete='cascade', help="Specify a product if this rule only applies to one product. Keep empty otherwise."), 'categ_id': fields.many2one('product.category', 'Product Category', ondelete='cascade', help="Specify a product category if this rule only applies to products belonging to this category or its children categories. Keep empty otherwise."), - 'min_quantity': fields.integer('Min. Quantity', required=True, + 'min_quantity': fields.integer('Min. Quantity', help="For the rule to apply, bought/sold quantity must be greater " "than or equal to the minimum quantity specified in this field.\n" "Expressed in the default unit of measure of the product." ), + 'applied_on': fields.selection([('3_global', 'Global'),('2_product_category', ' Product Category'), ('1_product', 'Product'), ('0_product_variant', 'Product Variant')], string="Apply On", required=True, + help='Pricelist Item applicable on selected option'), 'sequence': fields.integer('Sequence', required=True, help="Gives the order in which the pricelist items will be checked. The evaluation gives highest priority to lowest sequence and stops as soon as a matching item is found."), - 'base': fields.selection(_price_field_get, 'Based on', required=True, - size=-1, # here use size=-1 to store the values as integers - help='Base price for computation. \n Public Price: The base price will be the Sale/public Price. \n Vendor Section on Product or Cost Price : The base price will be the vendor price if it is set, otherwise it will be the cost price. \n Other Pricelist : Computation of the base price based on another Pricelist.'), + 'base': fields.selection([('list_price', 'Public Price'), ('standard_price', 'Cost'), ('pricelist', 'Other Pricelist')], string="Based on", required=True, + help='Base price for computation. \n Public Price: The base price will be the Sale/public Price. \n Cost Price : The base price will be the cost price. \n Other Pricelist : Computation of the base price based on another Pricelist.'), 'base_pricelist_id': fields.many2one('product.pricelist', 'Other Pricelist'), - + 'pricelist_id': fields.many2one('product.pricelist', 'Pricelist'), 'price_surcharge': fields.float('Price Surcharge', digits_compute= dp.get_precision('Product Price'), help='Specify the fixed amount to add or substract(if negative) to the amount calculated with the discount.'), - 'price_discount': fields.float('Price Discount', digits=(16,4)), + 'price_discount': fields.float('Price Discount', digits=(16,2)), 'price_round': fields.float('Price Rounding', digits_compute= dp.get_precision('Product Price'), help="Sets the price so that it is a multiple of this value.\n" \ @@ -483,19 +319,28 @@ class product_pricelist_item(osv.osv): digits_compute= dp.get_precision('Product Price'), help='Specify the minimum amount of margin over the base price.'), 'price_max_margin': fields.float('Max. Price Margin', digits_compute= dp.get_precision('Product Price'), help='Specify the maximum amount of margin over the base price.'), - 'company_id': fields.related('price_version_id','company_id',type='many2one', - readonly=True, relation='res.company', string='Company', store=True) + 'company_id': fields.related('pricelist_id','company_id',type='many2one', + readonly=True, relation='res.company', string='Company', store=True), + 'currency_id': fields.related('pricelist_id', 'currency_id', type='many2one', + readonly=True, relation='res.currency', string='Currency', store=True), + 'date_start': fields.date('Start Date', help="Starting date for the pricelist item validation"), + 'date_end': fields.date('End Date', help="Ending valid for the pricelist item validation"), + 'compute_price': fields.selection([('fixed', 'Fix Price'), ('percentage', 'Percentage (discount)'), ('formula', 'Formula')], select=True, default='fixed'), + 'fixed_price': fields.float('Fixed Price'), + 'percent_price': fields.float('Percentage Price'), + #functional fields used for usability purposes + 'name': fields.function(_get_pricelist_item_name_price, type="char", string='Name', multi='item_name_price', help="Explicit rule name for this pricelist line."), + 'price': fields.function(_get_pricelist_item_name_price, type="char", string='Price', multi='item_name_price', help="Explicit rule name for this pricelist line."), } + _defaults = { + 'base': 'list_price', + 'min_quantity': 1, + 'sequence': 5, + 'price_discount': 0, + 'applied_on': '3_global', + } _constraints = [ (_check_recursion, 'Error! You cannot assign the Main Pricelist as Other Pricelist in PriceList Item!', ['base_pricelist_id']), (_check_margin, 'Error! The minimum margin should be lower than the maximum margin.', ['price_min_margin', 'price_max_margin']) ] - - def product_id_change(self, cr, uid, ids, product_id, context=None): - if not product_id: - return {} - prod = self.pool.get('product.product').read(cr, uid, [product_id], ['code','name']) - if prod[0]['code']: - return {'value': {'name': prod[0]['code']}} - return {} diff --git a/addons/product/pricelist_view.xml b/addons/product/pricelist_view.xml index 9608b10c45f..7fd93edce64 100644 --- a/addons/product/pricelist_view.xml +++ b/addons/product/pricelist_view.xml @@ -7,77 +7,19 @@ - - product.pricelist.version.form - product.pricelist.version - -
- - - - - - - - - - - - - - - - - -
-
-
- - product.pricelist.version.tree - product.pricelist.version - - - - - - - - - - - - Pricelist Versions - ir.actions.act_window - product.pricelist.version - form - - -

- Click to add a pricelist version. -

- There can be more than one version of a pricelist, each of - these must be valid during a certain period of time. Some - examples of versions: Main Prices, 2010, 2011, Summer Sales, - etc. -

-
-
- - product.pricelist.item.tree product.pricelist.item - - + + - + + @@ -86,39 +28,55 @@ product.pricelist.item.form product.pricelist.item -
-