Browse Source

Add a new module 'pos_top_sellers'.

Add new statistics to point of sale report section to show your best selling products for all of your shops over a given date range.
pull/22/head
Peter Hahn 10 years ago
parent
commit
294c11aab5
  1. 21
      pos_top_sellers/__init__.py
  2. 73
      pos_top_sellers/__openerp__.py
  3. 72
      pos_top_sellers/i18n/de.po
  4. 3
      pos_top_sellers/ir.model.access.csv
  5. 291
      pos_top_sellers/pos_top_sellers.py
  6. 83
      pos_top_sellers/pos_top_sellers_view.xml
  7. 4
      pos_top_sellers/static/src/css/pos_top_sellers.css
  8. 143
      pos_top_sellers/static/src/js/pos_top_sellers.js
  9. 12
      pos_top_sellers/static/src/xml/pos_top_sellers.xml

21
pos_top_sellers/__init__.py

@ -0,0 +1,21 @@
# -*- coding: utf-8 -*-
###############################################################################
#
# Copyright (C) 2015 initOS GmbH & Co. KG (<http://www.initos.com>).
#
# 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 <http://www.gnu.org/licenses/>.
#
###############################################################################
from . import pos_top_sellers

73
pos_top_sellers/__openerp__.py

@ -0,0 +1,73 @@
# -*- coding: utf-8 -*-
###############################################################################
#
# Copyright (C) 2015 initOS GmbH & Co. KG (<http://www.initos.com>).
#
# 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 <http://www.gnu.org/licenses/>.
#
###############################################################################
{
'name': 'Point of sale topseller report.',
'version': '1.0',
'category': '',
'summary': 'Report topseller products in point of sale for all of your shops.',
'description': """
Report top selling products for point of sale
=============================================
This module adds 2 new reports to report sales done via point of sale for all of
your companies shops.
Top 40 sales by shop
--------------------
* List top 40 sold products in a given period for all of your shops.
* Click on a product to show sales for this product by shop on a daily basis over that timeframe.
Recent sales by shop for a given product
----------------------------------------
* Show sales for a product by shop on a daily basis over a given period.
* Select product by product default code.
* Show current stock and total quantity of sales for this product and period.
""",
'author': 'initOS GmbH & Co. KG',
'website': 'http://www.initos.com',
'depends': [
'web',
'web_listview_date_range_bar',
'sale',
'product',
'point_of_sale',
],
'data': [
'ir.model.access.csv',
'pos_top_sellers_view.xml',
],
'demo': [
],
'installable': True,
'auto_install': False,
'application': False,
'images': [
],
'css': [
'static/src/css/pos_top_sellers.css',
],
'js': [
'static/src/js/pos_top_sellers.js',
],
'qweb': [
'static/src/xml/pos_top_sellers.xml',
],
}

72
pos_top_sellers/i18n/de.po

@ -0,0 +1,72 @@
# Translation of OpenERP Server.
# This file contains the translation of the following modules:
# * pos_top_sellers
#
msgid ""
msgstr ""
"Project-Id-Version: OpenERP Server 7.0\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2015-04-16 07:54+0000\n"
"PO-Revision-Date: 2015-04-16 09:54+0100\n"
"Last-Translator: \n"
"Language-Team: \n"
"Language: de\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"X-Generator: Poedit 1.5.4\n"
#. module: pos_top_sellers
#: code:addons/pos_top_sellers/pos_top_sellers.py:117
#, python-format
msgid "Currently in stock"
msgstr "Lagerbestand"
#. module: pos_top_sellers
#: code:addons/pos_top_sellers/pos_top_sellers.py:214
#, python-format
msgid "QT"
msgstr "Menge"
#. module: pos_top_sellers
#: model:ir.model,name:pos_top_sellers.model_pos_top_sellers_shop_report
msgid "pos.top.sellers.shop.report"
msgstr ""
#. module: pos_top_sellers
#: code:addons/pos_top_sellers/pos_top_sellers.py:116
#, python-format
msgid "Sold"
msgstr "Verkäufe"
#. module: pos_top_sellers
#: model:ir.actions.act_window,name:pos_top_sellers.action_pos_top_sellers_shop_report
#: model:ir.ui.menu,name:pos_top_sellers.menu_pos_top_sellers_shop_report
#: view:pos.top.sellers.shop.report:0
msgid "Best sellers by shop"
msgstr "Verkaufscharts Shop"
#. module: pos_top_sellers
#: model:ir.actions.act_window,name:pos_top_sellers.action_pos_top_sellers_product_report
#: model:ir.ui.menu,name:pos_top_sellers.menu_pos_top_sellers_product_report
#: view:pos.top.sellers.product.report:0
msgid "Recent sales by product/shop"
msgstr "Verkäufe Produkt/Shop"
#. module: pos_top_sellers
#. openerp-web
#: code:addons/pos_top_sellers/static/src/xml/pos_top_sellers.xml:7
#, python-format
msgid "Product code:"
msgstr "Artikelnummer:"
#. module: pos_top_sellers
#: model:ir.model,name:pos_top_sellers.model_pos_top_sellers_product_report
msgid "pos.top.sellers.product.report"
msgstr ""
#~ msgid "Date from:"
#~ msgstr "Anfangsdatum:"
#~ msgid "Date end:"
#~ msgstr "Enddatum"

3
pos_top_sellers/ir.model.access.csv

@ -0,0 +1,3 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_pos_top_sellers_product_report,Access to pos.top.sellers.product.report,model_pos_top_sellers_product_report,point_of_sale.group_pos_manager,1,0,0,0
access_pos_top_sellers_shop_report,Access to pos.top.sellers.shop.report,model_pos_top_sellers_shop_report,point_of_sale.group_pos_manager,1,0,0,0

291
pos_top_sellers/pos_top_sellers.py

@ -0,0 +1,291 @@
# -*- coding: utf-8 -*-
##############################################################################
#
# OpenERP, Open Source Management Solution
# Copyright (C) 2010-2013 OpenERP s.a. (<http://openerp.com>).
# Copyright (C) 2015 initOS GmbH & Co. KG (<http://www.initos.com>).
#
# 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 <http://www.gnu.org/licenses/>.
#
##############################################################################
from openerp.osv import orm, fields
from openerp.tools.translate import _
from datetime import datetime, timedelta
from openerp.tools import DEFAULT_SERVER_DATE_FORMAT
from lxml import etree
class pos_top_sellers_product_report(orm.Model):
_name = 'pos.top.sellers.product.report'
# Create no table. Everything is created dynamically in this model
_auto = False
_columns = dict(date = fields.char(string='', readonly=True))
def get_product_code_for_id(self, cr, uid, id, context=None):
prod = self.pool['product.product'].browse(cr, uid, id, context)
return prod.default_code
def get_product_id_for_code(self, cr, uid, default_code, context=None):
ids = self.pool['product.product'].search(cr, uid, [('default_code','=', default_code)], context)
return ids[0] if ids else 0
def fields_view_get(self, cr, uid, view_id=None, view_type='form', context=None, toolbar=False):
res = super(pos_top_sellers_product_report, self).\
fields_view_get(cr, uid, view_id=view_id, view_type=view_type,
context=context, toolbar=toolbar)
if view_type == 'tree':
shop_model = self.pool['sale.shop']
shop_ids = shop_model.search(cr, uid, [])
arch = etree.XML(res['arch'])
tree = arch.xpath("//tree")
for shop in shop_model.browse(cr, uid, shop_ids):
# create fields for shop
qty_key = 'qty_' + str(shop.id)
res['fields'].update({
qty_key: dict(
string=shop.name,
type='integer',
readonly='True'
)
})
# add field to tree
etree.SubElement(tree[0], 'field', dict(
name=qty_key
))
res['arch'] = etree.tostring(arch)
return res
def _get_context_date_range(self, cr, uid, context=None):
"""
Check date range from context and create date range for the past
30 days if date range is missing in context.
"""
date_from = context and context.get('list_date_range_bar_start')
date_to = context and context.get('list_date_range_bar_end')
if not date_to:
date_to = fields.date.context_today(self, cr, uid, context=context)
if not date_from:
timestamp = datetime.strptime(date_to, DEFAULT_SERVER_DATE_FORMAT)
timestamp -= timedelta(days=30)
date_from = fields.date.context_today(self, cr, uid, context=context, timestamp=timestamp)
return (date_from, date_to)
def search(self, cr, user, args, offset=0, limit=None, order=None, context=None, count=False):
product_id = context and context.get('my_res_id')
date_from, date_to = self._get_context_date_range(cr, user, context=context)
res = []
if product_id and date_from and date_to:
d0 = datetime.strptime(date_from, DEFAULT_SERVER_DATE_FORMAT)
d1 = datetime.strptime(date_to, DEFAULT_SERVER_DATE_FORMAT)
# range depends on number of days in date range
num_days = abs((d1 - d0).days)+1
res = range(1, 1+2+num_days)
return res
def read(self, cr, user, ids, fields=None, context=None, load='_classic_read'):
# create empty result lines
res = [dict(id=id) for id in ids]
product_id = context and context.get('my_res_id')
date_from, date_to = self._get_context_date_range(cr, user, context=context)
if not (product_id and date_from and date_to):
return res
# first two lines are summary of sales and stock
res[0].update(date=_('Sold'))
res[1].update(date=_('Currently in stock'))
# remaining lines are sales top statistics per date
for shop_id in self.pool['sale.shop'].search(cr, user, [], context=context):
sql='''
select
date_trunc('day', dd)::date as date
,COALESCE(pd.qty, 0) as qty
from generate_series
( %(date_from)s
, %(date_to)s
, '1 day'::interval) as dd
left join (
select
po.date_order::date as date
,sum(pol.qty) as qty
from pos_order_line pol
join pos_order po
on po.id = pol.order_id
join product_product pp
on pp.id = product_id
join product_template pt
on pp.product_tmpl_id = pt.id
where pt.list_price > 0 and
shop_id = %(shop_id)s and product_id = %(product_id)s
group by
po.date_order::date
) as pd
on pd.date = dd.date
order by
date desc
'''
cr.execute(sql, dict(shop_id=shop_id, product_id=product_id,
date_from=date_from, date_to=date_to))
query_result = cr.fetchall()
qty_key = 'qty_' + str(shop_id)
line_id=2
for date, qty in query_result:
res[line_id].update({
'date': date,
qty_key: qty
})
total_qty = res[0].get(qty_key, 0) + qty
res[0].update({qty_key: total_qty})
line_id += 1
ctx = dict(context or {})
ctx.update({'shop': shop_id,
'states': ('done',),
'what': ('in', 'out')})
product = self.pool['product.product'].browse(cr, user, product_id, context=ctx)
res[1].update({qty_key: product.qty_available})
# postprocess
for line in res[2:]:
# transform date format
# fixme: note this uses the server locale not the user language set in odoo
# to really do this right we need to format this in JS on the client side
date = datetime.strptime(line['date'], '%Y-%m-%d')
line['date'] = date.strftime('%A, %x')
return res
class pos_top_sellers_shop_report(orm.Model):
_name = 'pos.top.sellers.shop.report'
# We do not have columns. Everything is created dynamically in this model
_auto = False
# by default list the top 40 products
_top_ten_limit = 40
def fields_view_get(self, cr, uid, view_id=None, view_type='form', context=None, toolbar=False):
res = super(pos_top_sellers_shop_report, self).\
fields_view_get(cr, uid, view_id=view_id, view_type=view_type,
context=context, toolbar=toolbar)
if view_type == 'tree':
shop_model = self.pool['sale.shop']
shop_ids = shop_model.search(cr, uid, [])
arch = etree.XML(res['arch'])
tree = arch.xpath("//tree")
for shop in shop_model.browse(cr, uid, shop_ids):
# create fields for shop
product_key = 'product_id_' + str(shop.id)
qty_key = 'qty_' + str(shop.id)
res['fields'].update({
product_key: dict(
string=shop.name,
type='many2one',
relation='product.product',
readonly='True',
),
qty_key: dict(
string=_('QT'),
type='integer',
readonly='True'
)
})
# add field to tree
etree.SubElement(tree[0], 'field', dict(
name=product_key,
widget="pos_top_sellers_product_col",
))
etree.SubElement(tree[0], 'field', dict(
name=qty_key
))
res['arch'] = etree.tostring(arch)
return res
def search(self, cr, user, args, offset=0, limit=None, order=None, context=None, count=False):
# ignore sorting, limit etc
return range(1,1+self._top_ten_limit)
def read(self, cr, user, ids, fields=None, context=None, load='_classic_read'):
date_from = context and context.get('list_date_range_bar_start')
date_to = context and context.get('list_date_range_bar_end')
# create empty result lines
res = [dict(id=id) for id in ids]
limit = len(res)
for shop_id in self.pool['sale.shop'].search(cr, user, [], context=context):
sql='''
select
product_id
,COALESCE(default_code, pt.name)
,sum(pol.qty) as qty
from pos_order_line pol
join pos_order po
on po.id = pol.order_id
join product_product pp
on pp.id = product_id
join product_template pt
on pp.product_tmpl_id = pt.id
where pt.list_price > 0 and
shop_id = %(shop_id)s
'''
if date_from:
sql += '''and po.date_order::date >= %(date_from)s '''
if date_to:
sql += '''and po.date_order::date <= %(date_to)s '''
sql += \
'''
group by
product_id
,COALESCE(default_code, pt.name)
order by
qty desc
fetch first %(limit)s rows only
'''
cr.execute(sql, dict(shop_id=shop_id, limit=limit,
date_from=date_from, date_to=date_to))
product_key = 'product_id_' + str(shop_id)
qty_key = 'qty_' + str(shop_id)
line_id=0
for product_id, default_code, qty in cr.fetchall():
res[line_id].update({
product_key: (product_id, default_code),
qty_key: qty
})
line_id += 1
return res

83
pos_top_sellers/pos_top_sellers_view.xml

@ -0,0 +1,83 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
##############################################################################
#
# OpenERP, Open Source Management Solution
# Copyright (C) 2010-2013 OpenERP s.a. (<http://openerp.com>).
# Copyright (C) 2015 initOS GmbH & Co. KG (<http://www.initos.com>).
#
# 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 <http://www.gnu.org/licenses/>.
#
##############################################################################
-->
<openerp>
<data>
<record id="view_pos_top_sellers_shop_report" model="ir.ui.view">
<field name="name">pos.top.sellers.shop.report</field>
<field name="model">pos.top.sellers.shop.report</field>
<field name="arch" type="xml">
<tree string="Best sellers by shop" create='false'/>
<!--
Content is created dynamically in fields_view_get()
Do not add fields here!
Do not inherit this view!
-->
</field>
</record>
<record id="action_pos_top_sellers_shop_report" model="ir.actions.act_window">
<field name="name">Best sellers by shop</field>
<field name="type">ir.actions.act_window</field>
<field name="res_model">pos.top.sellers.shop.report</field>
<field name="view_type">form</field>
<field name="view_mode">pos_top_sellers_shop_view</field>
<field name="target">current</field>
</record>
<record id="view_pos_top_sellers_product_report" model="ir.ui.view">
<field name="name">pos.top.sellers.product.report</field>
<field name="model">pos.top.sellers.product.report</field>
<field name="arch" type="xml">
<tree string="Recent sales by product/shop" create='false'>
<field name="date"/>
</tree>
<!--
Content is created dynamically in fields_view_get()
Do not add fields here!
Do not inherit this view!
-->
</field>
</record>
<record id="action_pos_top_sellers_product_report" model="ir.actions.act_window">
<field name="name">Recent sales by product/shop</field>
<field name="type">ir.actions.act_window</field>
<field name="res_model">pos.top.sellers.product.report</field>
<field name="view_type">form</field>
<field name="view_mode">pos_top_sellers_product_view</field>
<field name="target">current</field>
</record>
<menuitem name="Best sellers by shop" id="menu_pos_top_sellers_shop_report"
parent="point_of_sale.menu_point_rep" action="action_pos_top_sellers_shop_report"
sequence="40"/>
<menuitem name="Recent sales by product/shop" id="menu_pos_top_sellers_product_report"
parent="point_of_sale.menu_point_rep" action="action_pos_top_sellers_product_report"
sequence="41"/>
</data>
</openerp>

4
pos_top_sellers/static/src/css/pos_top_sellers.css

@ -0,0 +1,4 @@
.oe_pos_top_sellers_product_id{
margin-right: 1em;
}

143
pos_top_sellers/static/src/js/pos_top_sellers.js

@ -0,0 +1,143 @@
openerp.pos_top_sellers = function(instance)
{
var _t = instance.web._t,
_lt = instance.web._lt;
var QWeb = instance.web.qweb;
instance.web.list.columns.add(
'field.pos_top_sellers_product_col',
'instance.web.pos_top_sellers.PosTopSellersListClickableColumn');
instance.web.views.add(
'pos_top_sellers_shop_view',
'instance.web.pos_top_sellers.PosTopSellersShopListView'
);
instance.web.views.add(
'pos_top_sellers_product_view',
'instance.web.pos_top_sellers.PosTopSellersProductListView'
);
instance.web.pos_top_sellers = instance.web.pos_top_sellers || {};
instance.web.pos_top_sellers.PosTopSellersListClickableColumn =
instance.web.list.Column.extend({
_format: function (row_data, options)
{
var prod_id = row_data[this.id].value[0];
var prod_name = _.escape(row_data[this.id].value[1] || options.value_if_empty);
return _.str.sprintf('<a class="oe_form_uri" data-pos-t10-click-id="%s" >%s</a>',
prod_id, prod_name);
},
});
instance.web.ListView.List.include({
render: function()
{
var result = this._super(this, arguments),
self = this;
this.$current.delegate('a[data-pos-t10-click-id]',
'click', function()
{
// forward context from parent view
var ctx = self.dataset.get_context().eval();
$.extend(ctx, {
my_res_id: jQuery(this).data('pos-t10-click-id'),
});
self.view.do_action({
type: 'ir.actions.act_window',
res_model: 'pos.top.sellers.product.report',
view_type: 'form',
view_mode: 'tree',
views: [[false, 'pos_top_sellers_product_view']],
},
{
additional_context: ctx
}
);
});
return result;
},
});
instance.web.pos_top_sellers.PosTopSellersShopListView =
instance.web.web_listview_date_range_bar.DateRangeBar.extend({
init: function(parent, dataset, view_id, _options) {
_options.selectable = false
_options.sortable = false
_options.reorderable = false
_options.search_view = false
this._super.apply(this, arguments);
},
});
instance.web.pos_top_sellers.PosTopSellersProductListView =
instance.web.pos_top_sellers.PosTopSellersShopListView.extend({
init: function(parent, dataset, view_id, options) {
this._super.apply(this, arguments);
},
start:function(){
var tmp = this._super.apply(this, arguments);
var self = this;
var defs = [];
this.$el.parent().find('.oe_list_date_range_bar_start').
prepend(QWeb.render("pos_top_sellers_product_view_product_selector", {widget: this}));
this.$el.parent().find('.oe_pos_top_sellers_product_id_input').change(function() {
var product_default_code = this.value === '' ? null : this.value;
if( product_default_code ){
req = self.dataset.call('get_product_id_for_code', [product_default_code]);
req.then(
function(product_id){
if(product_id)
{
self.ViewManager.$el.find("span.oe_breadcrumb_item").text(product_default_code);
self.last_context["my_res_id"] = product_id;
self.do_search(self.last_domain, self.last_context, self.last_group_by);
}
}
);
}
});
var ctx = this.dataset.get_context().eval();
if ( ctx.my_res_id )
{
req = self.dataset.call('get_product_code_for_id', [ctx.my_res_id])
req.then(
function(product_default_code){
self.$el.parent().find('.oe_pos_top_sellers_product_id_input').val( product_default_code );
self.ViewManager.$el.find("span.oe_breadcrumb_item").text(product_default_code);
}
);
defs.push(req);
}
return $.when(tmp, defs);
},
style_for: function (record) {
var self = this;
var tmp = self._super.apply(this, arguments);
var id = record.attributes.id;
if( id == 1 )
{
tmp += "color: #FF0000"
}
if( id == 2 )
{
tmp += "color: #FFCC00"
}
return tmp;
},
});
};

12
pos_top_sellers/static/src/xml/pos_top_sellers.xml

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates id="template" xml:space="preserve">
<t t-name="pos_top_sellers_product_view_product_selector">
<div class="oe_form_dropdown_section oe_pos_top_sellers_product_id">
<span style="font-weight:bold;">Product code:</span>
<input class="oe_pos_top_sellers_product_id_input" size="30"/>
</div>
</t>
</templates>
Loading…
Cancel
Save