[MERGE] forward port of branch 9.0 up to 87a8896

This commit is contained in:
Christophe Simonis
2015-11-03 13:14:23 +01:00
61 changed files with 492 additions and 1169 deletions
+1 -1
View File
@@ -390,7 +390,7 @@ class AccountJournal(models.Model):
if vals.get('type') in ('bank', 'cash'):
# For convenience, the name can be inferred from account number
if not vals.get('name') and 'bank_acc_number' in vals:
vals['name'] = _('Bank') + ' ' + vals['bank_acc_number']
vals['name'] = vals['bank_acc_number']
# If no code provided, loop to find next available journal code
if not vals.get('code'):
@@ -818,7 +818,7 @@ class AccountBankStatementLine(models.Model):
ctx = dict(self._context, date=self.date)
for aml_dict in to_create:
aml_dict['move_id'] = move.id
aml_dict['date'] = self.statement_id.date
aml_dict['date'] = self.date
aml_dict['partner_id'] = self.partner_id.id
aml_dict['journal_id'] = self.journal_id.id
aml_dict['company_id'] = self.company_id.id
+3 -3
View File
@@ -120,8 +120,8 @@ class account_register_payments(models.TransientModel):
invoices = self.env[active_model].browse(active_ids)
if any(invoice.state != 'open' for invoice in invoices):
raise UserError(_("You can only register payments for open invoices"))
if any(inv.partner_id != invoices[0].partner_id for inv in invoices):
raise UserError(_("In order to pay multiple invoices at once, they must belong to the same partner."))
if any(inv.commercial_partner_id != invoices[0].commercial_partner_id for inv in invoices):
raise UserError(_("In order to pay multiple invoices at once, they must belong to the same commercial partner."))
if any(MAP_INVOICE_TYPE_PARTNER_TYPE[inv.type] != MAP_INVOICE_TYPE_PARTNER_TYPE[invoices[0].type] for inv in invoices):
raise UserError(_("You cannot mix customer invoices and vendor bills in a single payment."))
if any(inv.currency_id != invoices[0].currency_id for inv in invoices):
@@ -132,7 +132,7 @@ class account_register_payments(models.TransientModel):
'amount': abs(total_amount),
'currency_id': invoices[0].currency_id.id,
'payment_type': total_amount > 0 and 'inbound' or 'outbound',
'partner_id': invoices[0].partner_id.id,
'partner_id': invoices[0].commercial_partner_id.id,
'partner_type': MAP_INVOICE_TYPE_PARTNER_TYPE[invoices[0].type],
})
return rec
@@ -516,7 +516,7 @@
<separator/>
<filter domain="[('user_id','=',uid)]" help="My Invoices"/>
<group expand="0" string="Group By">
<filter name="group_by_partner_id" string="Partner" context="{'group_by':'partner_id'}"/>
<filter name="group_by_partner_id" string="Partner" context="{'group_by':'commercial_partner_id'}"/>
<filter string="Salesperson" context="{'group_by':'user_id'}"/>
<filter name="status" string="Status" context="{'group_by':'state'}"/>
<separator/>
@@ -189,9 +189,9 @@
<field name="payment_method_id" widget="radio" attrs="{'invisible': [('hide_payment_method', '=', True)]}"/>
<field name="payment_method_code" invisible="1"/>
<label for="amount"/>
<div name="amount_div">
<field name="amount" class="oe_inline"/>
<field name="currency_id" options="{'no_create': True, 'no_open': True}" groups="base.group_multi_currency" class="oe_inline" invisible="1"/>
<div name="amount_div" class="o_row">
<field name="amount"/>
<field name="currency_id" options="{'no_create': True, 'no_open': True}" groups="base.group_multi_currency"/>
</div>
</group>
<group>
@@ -33,9 +33,7 @@ class AssetAssetReport(models.Model):
dl.name as name,
dl.depreciation_date as depreciation_date,
a.date as date,
(CASE WHEN (select min(d.id) from account_asset_depreciation_line as d
left join account_asset_asset as ac ON (ac.id=d.asset_id)
where a.id=ac.id) = min(dl.id)
(CASE WHEN dlmin.id = min(dl.id)
THEN a.value
ELSE 0
END) as gross_value,
@@ -59,8 +57,9 @@ class AssetAssetReport(models.Model):
a.company_id as company_id
from account_asset_depreciation_line dl
left join account_asset_asset a on (dl.asset_id=a.id)
left join (select min(d.id) as id,ac.id as ac_id from account_asset_depreciation_line as d inner join account_asset_asset as ac ON (ac.id=d.asset_id) group by ac_id) as dlmin on dlmin.ac_id=a.id
group by
dl.amount,dl.asset_id,dl.depreciation_date,dl.name,
a.date, dl.move_check, a.state, a.category_id, a.partner_id, a.company_id,
a.value, a.id, a.salvage_value
a.value, a.id, a.salvage_value, dlmin.id
)""")
+238 -8
View File
@@ -20,7 +20,7 @@ bus.Bus = Widget.extend({
// bus presence
this.set("window_focus", true);
this.on("change:window_focus", self, function(e) {
this.on("change:window_focus", self, function() {
self.options.im_presence = self.get("window_focus");
});
$(window).on("focus", _.bind(this.window_focus, this));
@@ -40,9 +40,9 @@ bus.Bus = Widget.extend({
poll: function() {
var self = this;
self.activated = true;
var data = {'channels': self.channels, 'last': self.last, 'options' : self.options};
var data = {channels: self.channels, last: self.last, options : self.options};
session.rpc('/longpolling/poll', data, {shadow : true}).then(function(result) {
_.each(result, _.bind(self.on_notification, self));
self._notification_receive(result);
if(!self.stop){
self.poll();
}
@@ -53,6 +53,9 @@ bus.Bus = Widget.extend({
setTimeout(_.bind(self.poll, self), bus.ERROR_DELAY + (Math.floor((Math.random()*20)+1)*1000));
});
},
_notification_receive: function(notifications){
_.each(notifications, this.on_notification);
},
on_notification: function(notification) {
if (notification.id > this.last) {
this.last = notification.id;
@@ -60,9 +63,8 @@ bus.Bus = Widget.extend({
this.trigger("notification", [notification.channel, notification.message]);
},
add_channel: function(channel){
if(!_.contains(this.channels, channel)){
this.channels.push(channel);
}
this.channels.push(channel);
this.channels = _.uniq(this.channels);
},
delete_channel: function(channel){
this.channels = _.without(this.channels, channel);
@@ -74,10 +76,238 @@ bus.Bus = Widget.extend({
window_blur: function() {
this.set("window_focus", false);
},
update_option: function(key, value){
this.options[key] = value;
},
delete_option: function(key){
if(_.contains(_.keys(this.options), key)){
delete this.options[key];
}
},
});
// singleton
bus.bus = new bus.Bus();
/**
* CrossTabBus Widget
*
* Manage the communication before browser tab to allow only one tab polling for the others (performance improvement)
* When a tab is opened, and the start_polling method is called, the tab is signaling through the localStorage to the
* others. When a tab is closed, it signals its removing. If he was the master tab (the polling one), he choose another
* one in the list of open tabs. This one start polling for the other. When a notification is recieved from the poll, it
* is signaling through the localStorage too.
*
* localStorage used keys are :
* - bus.channels : shared public channel list to listen during the poll
* - bus.options : shared options
* - bus.notification : the received notifications from the last poll
* - bus.tab_list : list of opened tab ids
* - bus.tab_master : generated id of the master tab
*/
var CrossTabBus = bus.Bus.extend({
init: function(){
var self = this;
this._super.apply(this, arguments);
this.is_master = false;
tab_manager.register_tab(function () {
self.is_master = true;
self.start_polling();
});
on("storage", this.on_storage.bind(this));
if (this.is_master) {
setItem('bus.channels', this.channels);
setItem('bus.options', this.options);
} else {
this.channels = getItem('bus.channels', this.channels);
this.options = getItem('bus.options', this.options);
}
},
start_polling: function(){
if (this.is_master) {
this._super.apply(this, arguments);
}
},
_notification_receive: function(notifications){
if(this.is_master) { // broadcast to other tabs
setItem('bus.notification', notifications);
}
this._super.apply(this, arguments);
},
on_storage: function (e) {
// use the value of event to not read from
// localStorage (avoid race condition)
var value = e.newValue;
// notifications changed
if(e.key === 'bus.notification'){
var notifs = JSON.parse(value);
_.each(notifs, this.on_notification);
}
// update channels
if(e.key === 'bus.channels'){
this.channels = JSON.parse(value);
}
// update options
if(e.key === 'bus.options'){
this.options = JSON.parse(value);
}
},
add_channel: function(){
this._super.apply(this, arguments);
setItem('bus.channels', this.channels);
},
delete_channel: function(){
this._super.apply(this, arguments);
setItem('bus.channels', this.channels);
},
update_option: function(){
this._super.apply(this, arguments);
setItem('bus.options', this.options);
},
delete_option: function(){
this._super.apply(this, arguments);
setItem('bus.options', this.options);
},
});
//utility functions
function on(type, listener) {
if (window.addEventListener) {
window.addEventListener(type, listener);
} else { //IE8
window.attachEvent('on' + type, listener);
}
}
function getItem(key, defaultValue) {
var val = localStorage.getItem(key);
return val ? JSON.parse(val) : defaultValue;
}
function setItem(key, value) {
localStorage.setItem(key, JSON.stringify(value));
}
var tab_manager = {
peersKey: 'bus.peers',
masterKey: 'bus.master',
heartbeatKey: 'bus.heartbeat',
isMaster: false,
id: new Date().getTime() + ':' + (Math.random() * 1000000000 | 0),
register_tab: function (callback) {
this.callback = callback;
var peers = getItem(tab_manager.peersKey, {});
peers[tab_manager.id] = new Date().getTime();
setItem(tab_manager.peersKey, peers);
on('unload', function () {
// unload peer
var peers = getItem(tab_manager.peersKey, {});
delete peers[tab_manager.id];
setItem(tab_manager.peersKey, peers);
// unload master
if (tab_manager.isMaster) {
localStorage.removeItem(tab_manager.masterKey);
}
});
if (!localStorage[tab_manager.masterKey]) {
tab_manager.start_election();
}
on('storage', function(e) {
if (!e) { e = window.event;}
if (e.key !== tab_manager.masterKey) {
return;
}
if (e.newValue === null) { //master was unloaded
tab_manager.start_election();
}
});
tab_manager.heartbeat();
},
heartbeat: function () {
var current = new Date().getTime();
var pollPeriod = 10000;
var heartbeatValue = localStorage[tab_manager.heartbeatKey] || 0;
var peers = getItem(tab_manager.peersKey, {});
if ((parseInt(heartbeatValue) + 5000) < current) {
// Heartbeat is out of date. Electing new master
tab_manager.start_election();
}
if (tab_manager.isMaster) {
localStorage[tab_manager.heartbeatKey] = current;
//walk through all peers and kill old
var cleanedPeers = {};
for (var peerName in peers) {
if (peers[peerName] + 15000 > current) {
cleanedPeers[peerName] = peers[peerName];
}
}
setItem(tab_manager.peersKey, cleanedPeers);
pollPeriod = 1500;
} else {
//update own heartbeat
peers[tab_manager.id] = current;
setItem(tab_manager.peersKey, peers);
}
setTimeout(function(){
tab_manager.heartbeat();
}, pollPeriod);
},
start_election: function () {
if (tab_manager.isMaster) {
return;
}
//check who's next
var peers = getItem(tab_manager.peersKey, {});
var now = new Date().getTime();
var newMaster;
for (var peerName in peers) {
//check for dead peers
if (peers[peerName] + 15000 < now) {
continue;
}
newMaster = peerName;
break;
}
if (newMaster === tab_manager.id) {
//we're next in queue. Electing as master
setItem(tab_manager.masterKey, tab_manager.id);
setItem(tab_manager.heartbeatKey, new Date().getTime());
tab_manager.isMaster = true;
tab_manager.callback();
//removing master peer from queue
delete peers[newMaster];
setItem(tab_manager.peersKey, peers);
}
},
};
// bus singleton, depending of the browser :
// if supporting LocalStorage, there will be only one tab polling
if(typeof Storage !== "undefined"){
bus.bus = new CrossTabBus();
} else {
bus.bus = new bus.Bus();
}
return bus;
});
+4 -15
View File
@@ -353,14 +353,7 @@
<field name="create_date"/>
<field name="country_id"/>
<separator />
<filter string="Leads"
name="type_lead"
domain="[('type', '=', 'lead')]"/>
<filter string="Opportunities"
name="type_opportunity"
domain="[('type', '=', 'opportunity')]"/>
<separator/>
<filter string="My Opportunities"
<filter string="My Leads"
name="assigned_to_me"
domain="[('user_id','=',uid)]"
help="Leads that are assigned to me"/>
@@ -371,9 +364,6 @@
<filter string="Important Messages" name="message_needaction" domain="[('message_needaction','=',True)]"/>
<filter string="Archived" name="archived" domain="[('active', '=', False)]"/>
<separator />
<filter string="Followup" name="activities"
domain="[('date_action', '&lt;=', datetime.datetime.now().replace(hour=23, minute=59, second=59))]"/>
<separator />
<filter string="Exclude Opt Out"
name='not_opt_out' domain="[('opt_out', '=', False)]"
help="Leads that did not ask not to be included in mass mailing campaigns"/>
@@ -384,9 +374,8 @@
<filter name="customer" string="Customer" help="Partner" domain="[]" context="{'group_by':'partner_id'}"/>
<filter string="Campaign" name="groupby_campaign" domain="[]" context="{'group_by':'campaign_id'}"/>
<filter string="Source" name="groupby_source" domain="[]" context="{'group_by':'source_id'}"/>
<filter string="Channel" name="groupby_channel" domain="[]" context="{'group_by':'medium_id'}"/>
<filter string="Medium" name="groupby_channel" domain="[]" context="{'group_by':'medium_id'}"/>
<filter string="Creation Month" name="groupby_createmonth" domain="[]" context="{'group_by':'create_date'}"/>
<filter string="Expected Closing" name="groupby_expectclosing" domain="[]" context="{'group_by':'date_deadline'}"/>
</group>
</search>
</field>
@@ -576,7 +565,7 @@
<field name="name">Opportunities Tree</field>
<field name="model">crm.lead</field>
<field name="arch" type="xml">
<tree string="Opportunities" decoration-bf="message_needaction==True" decoration-muted="probability == 100" decoration-danger="date_deadline and (date_deadline &lt; current_date)">
<tree string="Opportunities" decoration-bf="message_needaction==True" decoration-muted="probability == 100" decoration-danger="date_action and (date_action &lt; current_date)">
<field name="date_deadline" invisible="1"/>
<field name="create_date"/>
<field name="name" string="Opportunity"/>
@@ -651,7 +640,7 @@
<filter string="Company" context="{'group_by':'company_id'}" groups="base.group_multi_company"/>
<filter name="stage" string="Stage" context="{'group_by':'stage_id'}"/>
<filter string="Campaign" domain="[]" context="{'group_by':'campaign_id'}"/>
<filter string="Channel" domain="[]" context="{'group_by':'medium_id'}"/>
<filter string="Medium" domain="[]" context="{'group_by':'medium_id'}"/>
<filter string="Source" domain="[]" context="{'group_by':'source_id'}"/>
<separator orientation="vertical" />
<filter string="Creation Month" context="{'group_by':'create_date:month'}" name="month"/>
+1 -1
View File
@@ -122,7 +122,7 @@ class crm_team(osv.Model):
def action_your_pipeline(self, cr, uid, context=None):
IrModelData = self.pool['ir.model.data']
action = IrModelData.xmlid_to_object(cr, uid, 'crm.crm_lead_opportunities_tree_view').read(['name', 'help', 'res_model', 'target', 'context', 'type'])
action = IrModelData.xmlid_to_object(cr, uid, 'crm.crm_lead_opportunities_tree_view').read(['name', 'help', 'res_model', 'target', 'domain', 'context', 'type', 'search_view_id'])
if not action:
action = {}
else:
@@ -29,6 +29,6 @@ class PriceRule(models.Model):
operator = fields.Selection([('==', '='), ('<=', '<='), ('<', '<'), ('>=', '>='), ('>', '>')], 'Operator', required=True, default='<=')
max_value = fields.Float('Maximum Value', required=True)
variable_factor = fields.Selection([('weight', 'Weight'), ('volume', 'Volume'), ('wv', 'Weight * Volume'), ('price', 'Price'), ('quantity', 'Quantity')], 'Variable Factor', required=True, default='weight')
list_base_price = fields.Float(string='Sale Base Price', digits_compute=dp.get_precision('Product Price'), required=True, default=0.0)
list_price = fields.Float('Sale Price', digits_compute=dp.get_precision('Product Price'), required=True, default=0.0)
standard_price = fields.Float('Cost Price', digits_compute=dp.get_precision('Product Price'), required=True, default=0.0)
list_base_price = fields.Float(string='Sale Base Price', digits=dp.get_precision('Product Price'), required=True, default=0.0)
list_price = fields.Float('Sale Price', digits=dp.get_precision('Product Price'), required=True, default=0.0)
standard_price = fields.Float('Cost Price', digits=dp.get_precision('Product Price'), required=True, default=0.0)
+1 -1
View File
@@ -13,7 +13,7 @@ class StockMove(models.Model):
uom_categ_id = self.env.ref('product.product_uom_categ_kgm').id
return self.env['product.uom'].search([('category_id', '=', uom_categ_id), ('factor', '=', 1)], limit=1)
weight = fields.Float(compute='_cal_move_weight', digits_compute=dp.get_precision('Stock Weight'), store=True)
weight = fields.Float(compute='_cal_move_weight', digits=dp.get_precision('Stock Weight'), store=True)
weight_uom_id = fields.Many2one('product.uom', string='Unit of Measure', required=True, readonly=True, help="Unit of Measure (Unit of Measure) is the unit of measurement for Weight", default=_default_uom)
@api.depends('product_id', 'product_uom_qty', 'product_uom')
+1 -1
View File
@@ -56,7 +56,7 @@ class StockPicking(models.Model):
delivery_type = fields.Selection(related='carrier_id.delivery_type', readonly=True)
carrier_id = fields.Many2one("delivery.carrier", string="Carrier")
volume = fields.Float(copy=False)
weight = fields.Float(compute='_cal_weight', digits_compute=dp.get_precision('Stock Weight'), store=True)
weight = fields.Float(compute='_cal_weight', digits=dp.get_precision('Stock Weight'), store=True)
carrier_tracking_ref = fields.Char(string='Tracking Reference', copy=False)
number_of_packages = fields.Integer(string='Number of Packages', copy=False)
weight_uom_id = fields.Many2one('product.uom', string='Unit of Measure', required=True, readonly="1", help="Unit of measurement for Weight", default=_default_uom)
@@ -42,7 +42,7 @@
<field name="view_type">form</field>
<field name="view_mode">tree,form</field>
<field name="context">{}</field>
<field name="domain">[('url', 'like', '/spreadsheet/')]</field>
<field name="domain">[('url', '=ilike', '%google%/spreadsheet%')]</field>
<field name="help">Google Spreadsheets</field>
</record>
@@ -14,7 +14,7 @@ class hr_timesheet_report(models.Model):
user_id = fields.Many2one('res.users', 'User', readonly=True)
account_id = fields.Many2one('account.analytic.account', 'Analytic Account', readonly=True)
company_id = fields.Many2one('res.company', 'Company', readonly=True)
cost = fields.Float('Cost', readonly=True, digits_compute=dp.get_precision('Account'))
cost = fields.Float('Cost', readonly=True, digits=dp.get_precision('Account'))
quantity = fields.Float('Time', readonly=True)
def _select(self):
@@ -44,7 +44,8 @@ index_template = """
</p>
<p>
For more information on how to setup the Point of Sale with
the PosBox, please refer to <a href='/hw_proxy/static/doc/manual.pdf'>the manual</a>.
the PosBox, please refer to
<a href='https://www.odoo.com/documentation/user/point_of_sale/posbox/index.html'>the manual</a>.
</p>
<p>
To see the status of the connected hardware, please refer
+2 -1
View File
@@ -70,7 +70,8 @@ upgrade_template = """
<p></p>
However the preferred method to upgrade the posbox is to flash the sd-card with
the <a href='http://nightly.odoo.com/trunk/posbox/'>latest image</a>. The upgrade
procedure is explained into to the <a href='/hw_proxy/static/doc/manual.pdf'>PosBox manual</a>
procedure is explained into to the
<a href='https://www.odoo.com/documentation/user/point_of_sale/posbox/index.html'>PosBox manual</a>
</p>
<p>
To upgrade the posbox, click on the upgrade button. The upgrade will take a few minutes. <b>Do not reboot</b> the PosBox during the upgrade.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 66 KiB

@@ -1,548 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="1600px"
height="1200px"
id="svg2985"
version="1.1"
inkscape:version="0.48.3.1 r9886"
sodipodi:docname="posbox_doc_schema.svg"
inkscape:export-filename="/home/fva/Code/openerp/posbox_doc_schema.png"
inkscape:export-xdpi="67.5"
inkscape:export-ydpi="67.5">
<defs
id="defs2987">
<marker
inkscape:stockid="DotL"
orient="auto"
refY="0.0"
refX="0.0"
id="DotL"
style="overflow:visible">
<path
id="path6730"
d="M -2.5,-1.0 C -2.5,1.7600000 -4.7400000,4.0 -7.5,4.0 C -10.260000,4.0 -12.5,1.7600000 -12.5,-1.0 C -12.5,-3.7600000 -10.260000,-6.0 -7.5,-6.0 C -4.7400000,-6.0 -2.5,-3.7600000 -2.5,-1.0 z "
style="fill-rule:evenodd;stroke:#000000;stroke-width:1.0pt"
transform="scale(0.8) translate(7.4, 1)" />
</marker>
<marker
inkscape:stockid="DotM"
orient="auto"
refY="0.0"
refX="0.0"
id="DotM"
style="overflow:visible">
<path
id="path6733"
d="M -2.5,-1.0 C -2.5,1.7600000 -4.7400000,4.0 -7.5,4.0 C -10.260000,4.0 -12.5,1.7600000 -12.5,-1.0 C -12.5,-3.7600000 -10.260000,-6.0 -7.5,-6.0 C -4.7400000,-6.0 -2.5,-3.7600000 -2.5,-1.0 z "
style="fill-rule:evenodd;stroke:#000000;stroke-width:1.0pt"
transform="scale(0.4) translate(7.4, 1)" />
</marker>
<pattern
inkscape:collect="always"
xlink:href="#Strips1_3"
id="pattern6512"
patternTransform="matrix(1.4070053,-1.4070053,10.196067,10.196067,0,0)" />
<pattern
inkscape:stockid="Stripes 1:3"
id="Strips1_3"
patternTransform="translate(0,0) scale(10,10)"
height="1"
width="4"
patternUnits="userSpaceOnUse"
inkscape:collect="always">
<rect
id="rect4622"
height="2"
width="1"
y="-0.5"
x="0"
style="fill:black;stroke:none" />
</pattern>
</defs>
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="1"
inkscape:pageshadow="2"
inkscape:zoom="0.45254834"
inkscape:cx="679.01909"
inkscape:cy="813.9637"
inkscape:current-layer="layer1"
inkscape:document-units="px"
showgrid="false"
inkscape:window-width="1920"
inkscape:window-height="991"
inkscape:window-x="0"
inkscape:window-y="27"
inkscape:window-maximized="1" />
<metadata
id="metadata2990">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<g
id="layer1"
inkscape:label="Layer 1"
inkscape:groupmode="layer">
<g
id="g7313">
<g
transform="translate(0,35.355339)"
id="g6566">
<g
id="g6514"
transform="translate(-10.9375,-57.8125)">
<path
inkscape:connector-curvature="0"
id="path6489"
d="m 905,179.59375 -34.8125,31.34375 -118.78125,0 c -5.54,0 -10,4.46 -10,10 l 0,102.5 -157.5,0 c -16.62,0 -30,13.38 -30,30 l 0,632.3125 -27.4375,24.9375 33.25,44.2813 c 0.10609,0.1442 0.2039,0.2952 0.3125,0.4374 l 2.125,2.8126 0.21875,-0.125 c 5.44486,5.6076 13.06148,9.0937 21.53125,9.0937 l 463.43755,0 c 16.6199,0 30,-13.38 30,-30 l 0,-683.75 c 0,-10.90688 -5.7475,-20.40588 -14.4063,-25.65625 l -41.125,-31 -27.3125,26.65625 -45.28125,0 0,-102.5 c 0,-2.89877 -1.22955,-5.48931 -3.1875,-7.3125 l 0.25,-0.375 L 905,179.59375 z"
style="opacity:0.25;fill:url(#pattern6512);fill-opacity:1;stroke:none" />
<g
transform="translate(-2.2097087,75.130096)"
id="g3823">
<g
id="g3847">
<rect
ry="5.274591"
y="106.16312"
x="711.8349"
height="151.06096"
width="194.27489"
id="rect3843"
style="fill:#ffffff;fill-opacity:1;stroke:#000000;stroke-width:8.48901749;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
rx="5.274591" />
<rect
rx="5.274591"
style="fill:none;stroke:#000000;stroke-width:2;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
id="rect3845"
width="175.41872"
height="151.06096"
x="721.263"
y="106.16312"
ry="5.274591" />
</g>
<rect
rx="5.274591"
ry="5.274591"
y="869.83844"
x="746.01172"
height="110.99157"
width="131.81378"
id="rect3765"
style="fill:#ffffff;fill-opacity:1;stroke:#000000;stroke-width:8.48901749;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" />
<rect
style="fill:#ffffff;fill-opacity:1;stroke:#000000;stroke-width:8.48901749;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
id="rect6618"
width="125.18465"
height="53.539146"
x="938.25635"
y="571.52777"
ry="5.274591"
rx="5.274591" />
<rect
rx="34.723557"
style="fill:#ffffff;fill-opacity:1;stroke:#000000;stroke-width:8;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
id="rect2993"
width="509.11688"
height="730.677"
x="525.61603"
y="217.12157"
ry="34.723557" />
<rect
rx="21.787001"
ry="21.787001"
y="235.37366"
x="543.0672"
height="694.12708"
width="474.21454"
id="rect3763"
style="fill:none;stroke:#000000;stroke-width:1.99425566;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" />
<rect
style="fill:none;stroke:#000000;stroke-width:1.99425554;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
id="rect3767"
width="160.64433"
height="9.1270761"
x="562.84052"
y="740.37366"
rx="5.3000002"
ry="5.3000002" />
<rect
ry="5.3000002"
rx="5.2999997"
y="667.30597"
x="828.00549"
height="9.1270761"
width="73.434502"
id="rect3769"
style="fill:none;stroke:#000000;stroke-width:1.99399996;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" />
<rect
ry="5.3000002"
rx="5.3000002"
y="313.75256"
x="693.65527"
height="9.1270761"
width="160.64433"
id="rect3771"
style="fill:none;stroke:#000000;stroke-width:1.99425554;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" />
<g
id="g3783">
<rect
ry="5.3000002"
rx="5.3000002"
y="245.39903"
x="962.35583"
height="271.93509"
width="43.971706"
id="rect3773"
style="fill:none;stroke:#000000;stroke-width:1.99425554;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" />
<rect
ry="5.3000002"
rx="5.3000002"
y="374.20874"
x="952.03491"
height="7.0710678"
width="21.213203"
id="rect3775"
style="fill:#ffffff;fill-opacity:1;stroke:none" />
<rect
style="fill:#ffffff;fill-opacity:1;stroke:none"
id="rect3777"
width="21.213203"
height="7.0710678"
x="992.86823"
y="373.3754"
rx="5.3000002"
ry="5.3000002" />
<rect
transform="matrix(0,-1,1,0,0,0)"
ry="5.3000002"
rx="5.3000002"
y="981.60596"
x="-260.01752"
height="7.0710678"
width="21.213203"
id="rect3779"
style="fill:#ffffff;fill-opacity:1;stroke:none" />
<rect
style="fill:#ffffff;fill-opacity:1;stroke:none"
id="rect3781"
width="21.213203"
height="7.0710678"
x="-526.6842"
y="981.60596"
rx="5.3000002"
ry="5.3000002"
transform="matrix(0,-1,1,0,0,0)" />
</g>
<path
inkscape:connector-curvature="0"
style="fill:#ffffff;fill-opacity:1;stroke:#000000;stroke-width:2;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
d="m 958.1875,822.25 c -9.01707,3.59482 -15.375,12.41301 -15.375,22.71875 0,13.50563 10.93187,24.46875 24.4375,24.46875 13.50563,0 24.46875,-10.96312 24.46875,-24.46875 0,-10.17788 -6.2141,-18.91637 -15.0625,-22.59375 l 0,8.8125 c 4.63325,2.9517 7.6875,8.02222 7.6875,13.78125 0,9.11223 -7.65608,16.5 -17.09375,16.5 -9.43767,0 -17.0625,-7.38777 -17.0625,-16.5 0,-5.88681 3.20248,-11.04837 8,-13.96875 l 0,-8.75 z"
id="path3792" />
<g
id="g3818"
transform="translate(0,-1.767767)">
<rect
ry="5.3000002"
rx="5.2999992"
y="879.66095"
x="943.13324"
height="8.7558908"
width="46.827415"
id="rect3794"
style="fill:none;stroke:#000000;stroke-width:1.99399996;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" />
<rect
style="fill:none;stroke:#000000;stroke-width:1.99399996;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
id="rect3798"
width="46.827415"
height="8.7558908"
x="943.13324"
y="895.68982"
rx="5.2999992"
ry="5.3000002" />
<rect
ry="5.3000002"
rx="5.2999992"
y="910.22754"
x="943.13324"
height="8.7558908"
width="46.827415"
id="rect3800"
style="fill:none;stroke:#000000;stroke-width:1.99399996;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" />
</g>
<rect
ry="5.3000002"
rx="5.2999992"
y="963.05292"
x="-845.58075"
height="8.7558908"
width="32.390652"
id="rect3812"
style="fill:#ffffff;fill-opacity:1;stroke:#000000;stroke-width:1.99400008;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
transform="matrix(0,-1,1,0,0,0)" />
</g>
</g>
<g
id="g6559"
transform="translate(0,-7.7339804)">
<path
transform="matrix(1,0,0,0.85333333,0,117.03125)"
d="m 1119.5313,856.64062 c 0,5.82488 -4.1974,10.54688 -9.375,10.54688 -5.1777,0 -9.375,-4.722 -9.375,-10.54688 0,-5.82487 4.1973,-10.54687 9.375,-10.54687 5.1776,0 9.375,4.722 9.375,10.54687 z"
sodipodi:ry="10.546875"
sodipodi:rx="9.375"
sodipodi:cy="856.64062"
sodipodi:cx="1110.1562"
id="path6539"
style="fill:#00fd28;fill-opacity:1;stroke:#000000;stroke-width:2.16506362;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
sodipodi:type="arc" />
<path
sodipodi:type="arc"
style="fill:#ff0d0d;fill-opacity:1;stroke:#000000;stroke-width:2.16506362;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
id="path6541"
sodipodi:cx="1110.1562"
sodipodi:cy="856.64062"
sodipodi:rx="9.375"
sodipodi:ry="10.546875"
d="m 1119.5313,856.64062 c 0,5.82488 -4.1974,10.54688 -9.375,10.54688 -5.1777,0 -9.375,-4.722 -9.375,-10.54688 0,-5.82487 4.1973,-10.54687 9.375,-10.54687 5.1776,0 9.375,4.722 9.375,10.54687 z"
transform="matrix(1,0,0,0.85333333,0,141.44531)" />
<path
transform="matrix(1,0,0,0.85333333,0,165.85937)"
d="m 1119.5313,856.64062 c 0,5.82488 -4.1974,10.54688 -9.375,10.54688 -5.1777,0 -9.375,-4.722 -9.375,-10.54688 0,-5.82487 4.1973,-10.54687 9.375,-10.54687 5.1776,0 9.375,4.722 9.375,10.54687 z"
sodipodi:ry="10.546875"
sodipodi:rx="9.375"
sodipodi:cy="856.64062"
sodipodi:cx="1110.1562"
id="path6543"
style="fill:#00fd28;fill-opacity:1;stroke:#000000;stroke-width:2.16506362;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
sodipodi:type="arc" />
<path
sodipodi:type="arc"
style="fill:#00fd28;fill-opacity:1;stroke:#000000;stroke-width:2.16506362;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
id="path6545"
sodipodi:cx="1110.1562"
sodipodi:cy="856.64062"
sodipodi:rx="9.375"
sodipodi:ry="10.546875"
d="m 1119.5313,856.64062 c 0,5.82488 -4.1974,10.54688 -9.375,10.54688 -5.1777,0 -9.375,-4.722 -9.375,-10.54688 0,-5.82487 4.1973,-10.54687 9.375,-10.54687 5.1776,0 9.375,4.722 9.375,10.54687 z"
transform="matrix(1,0,0,0.85333333,0,190.27344)" />
<path
transform="matrix(1,0,0,0.85333333,0,214.6875)"
d="m 1119.5313,856.64062 c 0,5.82488 -4.1974,10.54688 -9.375,10.54688 -5.1777,0 -9.375,-4.722 -9.375,-10.54688 0,-5.82487 4.1973,-10.54687 9.375,-10.54687 5.1776,0 9.375,4.722 9.375,10.54687 z"
sodipodi:ry="10.546875"
sodipodi:rx="9.375"
sodipodi:cy="856.64062"
sodipodi:cx="1110.1562"
id="path6547"
style="fill:#fff90d;fill-opacity:1;stroke:#000000;stroke-width:2.16506362;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
sodipodi:type="arc" />
</g>
</g>
<text
sodipodi:linespacing="125%"
id="text6598"
y="178.8759"
x="514.01447"
style="font-size:36.98903275px;font-style:normal;font-weight:normal;line-height:125%;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;font-family:Sans"
xml:space="preserve"><tspan
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:Droid Sans;-inkscape-font-specification:Droid Sans"
y="178.8759"
x="514.01447"
id="tspan6600"
sodipodi:role="line">2A Power</tspan></text>
<text
xml:space="preserve"
style="font-size:36.98903275px;font-style:normal;font-weight:normal;line-height:125%;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;font-family:Sans"
x="724.2699"
y="127.21583"
id="text6602"
sodipodi:linespacing="125%"><tspan
sodipodi:role="line"
id="tspan6604"
x="724.2699"
y="127.21583"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:Droid Sans;-inkscape-font-specification:Droid Sans">SD Card</tspan></text>
<text
sodipodi:linespacing="125%"
id="text6606"
y="626.60999"
x="319.89322"
style="font-size:36.98903275px;font-style:normal;font-weight:normal;line-height:125%;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;font-family:Sans"
xml:space="preserve"><tspan
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:Droid Sans;-inkscape-font-specification:Droid Sans"
y="626.60999"
x="319.89322"
id="tspan6608"
sodipodi:role="line">HDMI</tspan></text>
<text
xml:space="preserve"
style="font-size:36.98903275px;font-style:normal;font-weight:normal;line-height:125%;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;font-family:Sans"
x="761.83496"
y="1097.278"
id="text6610"
sodipodi:linespacing="125%"><tspan
sodipodi:role="line"
id="tspan6612"
x="761.83496"
y="1097.278"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:Droid Sans;-inkscape-font-specification:Droid Sans">USB</tspan></text>
<text
sodipodi:linespacing="125%"
id="text6614"
y="1097.278"
x="596.10681"
style="font-size:36.98903275px;font-style:normal;font-weight:normal;line-height:125%;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;font-family:Sans"
xml:space="preserve"><tspan
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:Droid Sans;-inkscape-font-specification:Droid Sans"
y="1097.278"
x="596.10681"
id="tspan6616"
sodipodi:role="line">RJ45</tspan></text>
<text
xml:space="preserve"
style="font-size:36.98903275px;font-style:normal;font-weight:normal;line-height:125%;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;font-family:Sans"
x="1135.8894"
y="660.90717"
id="text6620"
sodipodi:linespacing="125%"><tspan
sodipodi:role="line"
id="tspan6622"
x="1135.8894"
y="660.90717"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:Droid Sans;-inkscape-font-specification:Droid Sans">Composite</tspan></text>
<text
sodipodi:linespacing="125%"
id="text6624"
y="774.18842"
x="1135.8894"
style="font-size:36.98903275px;font-style:normal;font-weight:normal;line-height:125%;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;font-family:Sans"
xml:space="preserve"><tspan
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:Droid Sans;-inkscape-font-specification:Droid Sans"
y="774.18842"
x="1135.8894"
id="tspan6626"
sodipodi:role="line">Audio</tspan></text>
<text
xml:space="preserve"
style="font-size:36.98903275px;font-style:normal;font-weight:normal;line-height:125%;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;font-family:Sans"
x="1135.8894"
y="774.18842"
id="text6628"
sodipodi:linespacing="125%"><tspan
sodipodi:role="line"
id="tspan6630"
x="1135.8894"
y="774.18842"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:Droid Sans;-inkscape-font-specification:Droid Sans">Audio</tspan></text>
<text
sodipodi:linespacing="125%"
id="text6632"
y="891.37592"
x="1212.4519"
style="font-size:36.98903275px;font-style:normal;font-weight:normal;line-height:125%;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;font-family:Sans"
xml:space="preserve"><tspan
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:Droid Sans;-inkscape-font-specification:Droid Sans"
y="891.37592"
x="1212.4519"
id="tspan6634"
sodipodi:role="line">Ready</tspan></text>
<text
xml:space="preserve"
style="font-size:36.98903275px;font-style:normal;font-weight:normal;line-height:125%;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;font-family:Sans"
x="1212.4519"
y="940.45667"
id="text6636"
sodipodi:linespacing="125%"><tspan
sodipodi:role="line"
id="tspan6638"
x="1212.4519"
y="940.45667"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:Droid Sans;-inkscape-font-specification:Droid Sans">Power</tspan></text>
<text
sodipodi:linespacing="125%"
id="text6640"
y="986.97278"
x="1212.4519"
style="font-size:36.98903275px;font-style:normal;font-weight:normal;line-height:125%;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;font-family:Sans"
xml:space="preserve"><tspan
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:Droid Sans;-inkscape-font-specification:Droid Sans"
y="986.97278"
x="1212.4519"
id="tspan6642"
sodipodi:role="line">Network</tspan></text>
<path
inkscape:connector-curvature="0"
id="path6663"
d="m 437.5,614.15728 54.6875,0"
style="fill:none;stroke:#000000;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;marker-start:url(#DotL)" />
<path
style="fill:none;stroke:#000000;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;marker-start:url(#DotL)"
d="m 601.86625,199.79103 0,54.6875"
id="path7295"
inkscape:connector-curvature="0" />
<path
inkscape:connector-curvature="0"
id="path7297"
d="m 1113.1075,650.87603 -54.6875,0"
style="fill:none;stroke:#000000;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;marker-start:url(#DotL)" />
<path
sodipodi:nodetypes="cc"
style="fill:none;stroke:#000000;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;marker-start:url(#DotL)"
d="m 1113.1075,761.81353 -76.5625,0"
id="path7299"
inkscape:connector-curvature="0" />
<path
style="fill:none;stroke:#000000;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;marker-start:url(#DotL)"
d="m 1192.657,876.26632 -54.6875,0"
id="path7301"
inkscape:connector-curvature="0" />
<path
sodipodi:nodetypes="cc"
inkscape:connector-curvature="0"
id="path7303"
d="m 1192.657,927.08963 -54.6875,-27.62136"
style="fill:none;stroke:#000000;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;marker-start:url(#DotL)" />
<path
style="fill:none;stroke:#000000;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;marker-start:url(#DotL)"
d="m 1192.657,974.04593 -54.6875,0"
id="path7305"
inkscape:connector-curvature="0" />
<path
sodipodi:nodetypes="cc"
inkscape:connector-curvature="0"
id="path7307"
d="m 1192.657,974.04593 -54.6875,-24.85922"
style="fill:none;stroke:#000000;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;marker-start:url(#DotL)" />
<path
style="fill:none;stroke:#000000;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;marker-start:url(#DotL)"
d="m 1192.657,974.04593 -54.6875,-49.16602"
id="path7309"
inkscape:connector-curvature="0"
sodipodi:nodetypes="cc" />
<path
sodipodi:nodetypes="cc"
style="fill:none;stroke:#000000;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;marker-start:url(#DotL)"
d="m 630.03792,1053.883 0,-32.0313"
id="path7311"
inkscape:connector-curvature="0" />
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 24 KiB

-447
View File
@@ -1,447 +0,0 @@
====================
PosBox Documentation
====================
Posbox Setup Guide
==================
Prerequisites
-------------
Before you start setting up your PosPox make sure you have everything.
You will need :
- The PosBox
- A 2A Power adapter
- A computer or tablet with an up to date Firefox, Chrome or Safari web
browser (we recommend Chrome)
- A running SaaS or V8.0 OpenERP Instance with the Point of Sale
Installed.
- A local network set up with DHCP ( this is the default setting )
- A RJ45 Ethrnet Cable
- An Epson USB TM-T20 Printer or another compatible printer.
- A Honeywell Eclipse USB Barcode Scanner or another compatible scanner.
- An Epson compatible cash drawer.
Step By Step Setup Guide
------------------------
.. image:: _images/posbox_doc_schema.png
:width: 100%
:align: center
Power the PosBox
~~~~~~~~~~~~~~~~
Plug the PosBox to the 2A Power Adapter, a bright red status led should
light up.
Connect it to the Local Network
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Connect the PosBox to the Local Network with the RJ45 Cable. Make sure
You connect the PosBox to the same Network as your POS device. Once
connected a bright yellow status led should light up, and a green status
led should flash.
Connect the USB Printer
~~~~~~~~~~~~~~~~~~~~~~~
Put a paper roll in the printer, power it up, then connect it to one of
the PosBox's USB port
Connect the cash drawer
~~~~~~~~~~~~~~~~~~~~~~~
The cash drawer should be connected to the printer with the RJ25 cable
Connect the USB Barcode Scanner
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Connect the usb barcode scanner to one of the PosBox's USB port. The
Barcode Scanner should immediately light up and emit a loud beep.
Configure the USB Barcode Scanner
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
The barcode scanner should be configured in QWERTY and emit a new line /
return symbol after each scan. This is most likely the default
configuration of your barcode scanner.
Make sure the PosBox is ready
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Once powered, The PosBox needs less than a minute to boot. Once the
PosBox is ready, it should print a status receipt with its IP Adress.
Also, the last status led, just after the red power status led, should
be permanently lit green.
Setup the Point of Sale
~~~~~~~~~~~~~~~~~~~~~~~~
Make sure to activate the 'Print via Proxy', 'Remote Scanning' or
'Cashdrawer' options in the Point of Sale configuration. If you are
using firefox, you must manually specify the PosBox's IP address which
was printed on the status receipt.
If you are running multiple Point of Sales on the same PosBox, make sure
that only one of them has Remote Scanning activated.
If you manually specify the PosBox's IP address, you must configure your
network to make sure the PosBox's IP address doesn't change. Please
refer to your Router documentation.
Launch the Point of Sale.
~~~~~~~~~~~~~~~~~~~~~~~~~
If you didn't spcecify the PosBox's IP address in the config, the POS
will need some time to perform a network scan to find the PosBox. This
is only done once.
The Point of Sale is now connected to the PosBox and your Hardware
should be ready to use.
Multi-Pos Configuration
-----------------------
The advised way to setup a multi Point-of-Sale shop is to have one PosBox
per Point-of-Sale. In this case it is mandatory to manually specify the
IP address of each PosBox in each Point of Sale. You must also configure
your network to make sure the PosBox's IP address doesn't change. Please
refer to your Router documentation.
PosBoxless Setup Guide
======================
If you are running your Point of Sale on a debian-based linux
distribution, you do not need the PosBox as you can run its software
locally. However the installation process is not foolproof. You'll need
at least to know how to install and run openerp. You may also run into
issues specific to your distribution or to your particular setup and
hardware configuration.
Prerequisites
-------------
- A debian based linux distribution (Debian, Ubuntu, Mint, etc.)
- A running OpenERP server (version trunk or saas-3 or 8.0 or later)
- You must uninstall any esc-pos printer driver as it will conflict
with OpenERP's built-in driver.
Step By Step Setup Guide
------------------------
Extra dependencies
~~~~~~~~~~~~~~~~~~
Because Odoo is running on Python 2, you need to check which version of pip you need to use.
$ sudo pip --version
If it returns something like :
pip 1.5.6 from /usr/local/lib/python3.3/dist-packages/pip-1.5.6-py3.3.egg (python 3.3)
You need to try pip2 instead
If it returns something like :
pip 1.4.1 from /usr/lib/python2.7/dist-packages (python 2.7)
You can use pip
The driver modules requires the installation of new python modules::
$ sudo pip install pyserial
$ sudo pip install --pre pyusb
$ sudo pip install qrcode
Database Setup
~~~~~~~~~~~~~~
You must create a database called ``posbox`` with the modules
``hw_proxy``, ``hw_escpos``, and ``hw_scanner`` installed.
Access Rights
~~~~~~~~~~~~~
The drivers need raw access to the printer and barcode scanner devices.
Doing so requires a bit system administration. First we are going to
create a group that has haccess to usb devices::
$ sudo groupadd usbusers
Then we add the user who will run the OpenERP server to ``usbusers``::
$ sudo useradd -G usbusers USERNAME
Then we need to create a udev rule that will automatically allow members
of ``usbusers`` to access raw usb devices. To do so create a file called
``99-usbusers.rule`` in the ``/etc/udev/rules.d/`` directory with the
following content::
SUBSYSTEM=="usb", GROUP="usbusers", MODE="0660"
SUBSYSTEMS=="usb", GROUP="usbusers", MODE="0660"
Then you need to reboot your machine.
Start the local OpenERP Installl
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
We must launch the OpenERP server on the port ``8069`` with the correct
database settings::
$ ./server/openerp-server --addons-path=addons,web/addons --db-filter='^posbox$' \
--xmlrpc-port=8069 -d posbox
Check that everything works
~~~~~~~~~~~~~~~~~~~~~~~~~~~
Plug all your hardware to your machine's USB ports, and go to
``http://localhost:8069/hw_proxy/status`` refresh the page a few times and
see if all your devices are indicated as *Connected*. Possible source of
errors are: The paths on the distribution differ from the paths expected
by the drivers, another process has grabbed exclusive access to the
devices, the udev rules do not apply or a superceeded by others.
Automatically Start OpenERP
~~~~~~~~~~~~~~~~~~~~~~~~~~~
You must now make sure that this OpenERP install is automatically
started after boot. There are various ways to do so, and how to do it
depends on your particular setup. We use *systemd* on the PosBox, but
*upstart* or *sysvinit* are other options.
Setup the Point of Sale
~~~~~~~~~~~~~~~~~~~~~~~~
The IP Adress field in the POS Config must be either ``127.0.0.1``
or ``localhost``. You can also leave it empty.
PosBox Technical Documentation
==============================
Technical Overview
------------------
The PosBox Hardware
~~~~~~~~~~~~~~~~~~~
The PosBox's Hardware is based on a model B Raspberry Pi, a popular
open-source micro-computer. The Raspberry Pi is powered with a 2A
micro-usb power adapter. 2A is needed to give enough power to the
barcode scanners. And we recommend Samsung power adapters for their
availability and reliability ( but beware of counterfeits ). The
Raspberry is protected by a ModMyPi Raspberry Pi Case. The Software is
installed on a 8Gb Class 10 or Higher SD Card. The SD Card's class is
important to ensure good performances. All this hardware is easily
available worldwide from independant vendors.
Compatible Peripherals
~~~~~~~~~~~~~~~~~~~~~~
- Printers:
- Epson TM-T20
- Support for Other Esc-Pos compatible printers can be easily added
on a case-by-case basis, please see the *Unsupported Printers*
chapter of this manual.
- Barcode Scannners:
- Metapace S61
- Honeywell Eclipse or Voyager 95x0 Series.
- Most other barcode scanners should work out of the box. Some
barcode scanners need more power than the PosBox can provide and
must be plugged in a self-powered USB HUB. This is the case for
Datalogic Barcode Scanners.
- Cash Drawers:
- As the cash drawers are connected to the printer, All Epson
compatible cash drawers should work out of the Box.
The PosBox Software
~~~~~~~~~~~~~~~~~~~
The PosBox runs a Raspbian Linux distribution, a Debian derivative
optimized for the Raspberry Pi. It also runs a barebones install of
OpenERP which provides the webserver and the drivers. The printer &
scanner drivers are implemented as openerp modules. Those modules are
named ``hw_proxy``, ``hw_escpos``, ``hw_scanner`` and are the only
modules installed and running. OpenERP is only used for the framework it
provides. No business data is processed or stored on the PosBox. The
OpenERP install is a full bazaar clone of the ``trunk`` branch, and can
thus be updated trough the usual means.
We use systemd to manage the Openerp server. Systemd makes sure OpenERP
starts up at boot and is always up and running. Its systemd unit file is
called ``openerp.service``, and can be found in
``/etc/systemd/system/openerp.service``. The systemd version used is
quite old and thus ``journalctl`` is not available. All logs can be
found in instead ``/var/logs/syslog``
We removed all graphical software from the default install to reduce to
image size but nothing prevents you from reinstalling them.
Accessing the PosBox
--------------------
Local Access
~~~~~~~~~~~~
If you plug a QWERTY USB keyboard into one of the PosBox's USB ports,
and if you connect a computer monitor to the *HDMI* port of the PosBox,
you can use it as a small UNIX computer and perform various
administration tasks.
Once the PosBox is ready press ``ALT-F2`` to access the login prompt.
The login is ``pi`` and the password ``admin``. The OpenERP install is
in the ``~/openerp`` directory along with a few scripts to help with
debugging and administration.
Remote Access
~~~~~~~~~~~~~
If you have the PosBox's IP address and a SSH client you can access the
PosBox's system remotely. The login / password are ``pi``/``admin``
Getting Unsupported Printers to Work
------------------------------------
The PosBox should be able to print to any ESC-POS printer, not just the
Epson TM-T20. If You have such a printer, you can activate it with the
following steps:
- Get local or remote access to the PosBox.
- Plug in your printer
- type ``lsusb`` in a prompt
- Find your printer in the list of connected USB devices
- Find your printer's vendor id: It consists of two hexadecimal numbers
separated by a colon.
- Edit ``~/openerp/addons/hw_escpos/escpos/supported_devices.py`` and
add an entry for your printer.
- Restart the PosBox.
- If everything works properly you can send your printer's name and
vendor ID to ``support@openerp.com`` and we'll add it to the list of
supported devices.
Updating The PosBox Software
----------------------------
The best way to update the PosBox software is to download a new version
of the image and flash the SD-Card with it. This operation is described
in details on the following tutorial
``http://elinux.org/RPi_Easy_SD_Card_Setup``, just replace the standard
raspberry pi image to the latest one found at
``http://nightly.openerp.com/trunk/posbox/``
Troubleshoot
============
The POS cannot connect to the PosBox.
-------------------------------------
- The easiest way to make sure the PosBox is properly set-up is to turn
it on with the printer plugged in as it will print a receipt
indicating any error if encountered or the PosBox's IP adress in case
of success. If no receipt is printed, check the following steps:
- Make sure the PosBox is powered on, indicated by a brightly lid red
status LED.
- Make sure the PosBox is ready, this is indicated by a brightly lid
green status LED just above the red power status LED. The PosBox
should be ready one minute after it is powered on.
- Make sure the PosBox is connected to the Network. This is indicated
by a brightly lid yellow status LED.
- Make sure the PosBox is connected to the same network as your POS
device. Both the device and the posbox should be visible in the list
of connected devices on your network router.
- Make sure that your LAN is set up with DHCP, and gives ip Address in
the range 192.168.0.X, 192.168.1.X, 10.0.0.X. If you cannot setup
your LAN that way, you must manually set up your PosBox's ip-address.
See the relevant paragraph in the Setup chapter of this documentation
- If you have specified the PosBox's IP address in the config, make
sure it correspond to the printed on the PosBox's status receipt.
- Make sure that the POS is not loaded over HTTPS.
- A bug in Firefox's HTTP implementation prevents the autodiscovery
from working reliably. When using Firefox you should manually set up
the PosBox's ip address in the POS config.
The Barcode Scanner is not working
----------------------------------
- The PosBox needs a 2A power supply to work with some barcode
scanners. If you are not using the provided power supply, make sure
the one you use has enough power.
- Some barcode scanners will need more than 2A and will not work, or
will work unreliably, even with the provided power supply. In those
case you can plug the barcode scanner in a self-powered USB Hub.
- Some poorly built barcode scanners do not advertise themselves as
barcode scanners but as a usb keyboard instead, and will not be
recognized by the PosBox.
- The barcode scanner must be configured in US QWERTY and emit a linefeed
after each codebar.
The Barcode Scanner is not working reliably
-------------------------------------------
- Make sure that no more than one device with 'Scan via Proxy' enabled
are connected to the PosBox at the same time.
Printing the receipt takes too much time.
-----------------------------------------
- A small delay before the first print is expected, as the PosBox will
do some preprocessing to speed up the next printings. If you suffer
delays afterwards it is most likely due to poor network connection
between the POS and the PosBox.
Some characters are not correctly printed on the receipt.
---------------------------------------------------------
- The PosBox does not support all languages and characters. It
currently supports latin and cyrillic based scripts, with basic
japanese support.
The Printer is Offline
----------------------
- The PosBox only supports EPSON TM-T20 printers. Make sure the printer
is connected, powered, has enough paper and has its lid closed, and
does is not in an error status. If the error persists, please contact
support.
The Cashdrawer does not open.
-----------------------------
- The cashdrawer should be connected to the printer and should be
activated in the POS Configuration
Credits
=======
The PosBox project was developped by Frédéric van der Essen with the
kind help of Gary Malherbe, Fabien Meghazi, Nicolas Wisniewsky,
Dimitri Del Marmol and Antony Lesuisse.
This development would not have been possible without the Indiegogo
campaign and those who contributed to it. Special thanks goes to the
partners who backed the campaign with founding partner bundles:
- Camptocamp
- BHC
- openBig
- Eeezee-IT
- Solarsis LDA
- ACSONE
- Vauxoo
- Ekomurz
- Datalp
- Dao Systems
- Eggs Solutions
- OpusVL
And also the partners who've backed the development with the Founding
PosBox Bundle:
- Willow IT
- E\. Akhalwaya & Sons
- Multibase
- Mindesa
- bpso.biz
- Shine IT.
Binary file not shown.
Binary file not shown.
+2 -2
View File
@@ -142,7 +142,7 @@ class LunchOrderLine(models.Model):
readonly=True, store=True)
note = fields.Text('Note')
price = fields.Float(related='product_id.price', readonly=True, store=True,
digits_compute=dp.get_precision('Account'))
digits=dp.get_precision('Account'))
state = fields.Selection([('new', 'New'),
('confirmed', 'Received'),
('ordered', 'Ordered'),
@@ -192,7 +192,7 @@ class LunchProduct(models.Model):
name = fields.Char('Product', required=True)
category_id = fields.Many2one('lunch.product.category', 'Category', required=True)
description = fields.Text('Description')
price = fields.Float('Price', digits_compute=dp.get_precision('Account'))
price = fields.Float('Price', digits=dp.get_precision('Account'))
supplier = fields.Many2one('res.partner', 'Vendor')
+1 -1
View File
@@ -118,7 +118,7 @@ class MailController(http.Controller):
if not record_sudo:
# record does not seem to exist -> redirect to login
return self._redirect_to_messaging()
record_action = record_sudo.get_access_action()[0]
record_action = record_sudo.get_access_action()
# the record has an URL redirection: use it directly
if record_action['type'] == 'ir.actions.act_url':
+5 -3
View File
@@ -121,13 +121,15 @@
<td style="padding:0px 10px 5px 5px;font-size: 14px;">
${object.body | safe}
% if ctx.get('tracking'):
<ul>
% for tracking in ctx['tracking']
${tracking[0]} : ${tracking[1]} -> ${tracking[2]}
<li>${tracking[0]} : ${tracking[1]} -> ${tracking[2]}</li>
% endfor
</ul>
% endif
</td>
</tr>
</tbody></table>
</tr></tbody>
</table>
</div>
<div style="padding:0px; width:600px; max-width:600px; margin:0 auto; background: #FFFFFF repeat top /100%;color:#777777">
<table cellspacing="0" cellpadding="0" style="width:600px ;text-align:justify; margin:0 auto; border-collapse:collapse; background:inherit; color:inherit">
+1 -18
View File
@@ -6,13 +6,7 @@
<menuitem
name='Payments'
id='root_payment_menu'
parent='account.menu_finance_reports'/>
<!-- Payment menu in invoicing -->
<menuitem
name='Payments'
id='finance_payment_menu'
parent='account.menu_finance_reports'/>
parent='account.menu_finance_configuration'/>
<!-- Payment acquirers -->
<record id="payment_transaction_action_child" model="ir.actions.act_window">
@@ -231,12 +225,6 @@
parent='root_payment_menu'
sequence='20' />
<menuitem
action='action_payment_transaction'
id='finance_pay_tx_menu'
parent='finance_payment_menu'
sequence='20' />
<!-- Payment Methods -->
<record model='ir.ui.view' id='payment_method_tree_view'>
<field name='name'>payment.method.tree</field>
@@ -308,10 +296,5 @@
id='payment_method_menu'
parent='root_payment_menu'/>
<menuitem
action='payment_method_action'
id='finance_pay_method_menu'
parent='finance_payment_menu' />
</data>
</openerp>
+15 -1
View File
@@ -15,7 +15,7 @@ class AdyenController(http.Controller):
_return_url = '/payment/adyen/return/'
@http.route([
'/payment/adyen/return/',
'/payment/adyen/return',
], type='http', auth='none')
def adyen_return(self, **post):
_logger.info('Beginning Adyen form_feedback with post data %s', pprint.pformat(post)) # debug
@@ -26,3 +26,17 @@ class AdyenController(http.Controller):
custom = json.loads(post.pop('merchantReturnData', '{}'))
return_url = custom.pop('return_url', '/')
return werkzeug.utils.redirect(return_url)
@http.route([
'/payment/adyen/notification',
], type='http', auth='none', methods=['POST'])
def adyen_notification(self, **post):
tx_id = post.get('merchantReference') and request.registry['payment.transaction'].search(request.cr, SUPERUSER_ID, [('reference', 'in', [post.get('merchantReference')])], limit=1, context=request.context)
if post.get('eventCode') in ['AUTHORISATION'] and tx_id:
tx = request.registry['payment.transaction'].browse(request.cr, SUPERUSER_ID, tx_id, context=request.context)
states = (post.get('merchantReference'), post.get('success'), tx.state)
if (post.get('success') == 'true' and tx.state == 'done') or (post.get('success') == 'false' and tx.state in ['cancel', 'error']):
_logger.info('Notification from Adyen for the reference %s: received %s, state is %s', states)
else:
_logger.warning('Notification from Adyen for the reference %s: received %s but state is %s', states)
return '[accepted]'
+5 -2
View File
@@ -74,7 +74,7 @@ class pos_config(osv.osv):
result = dict()
for record in self.browse(cr, uid, ids, context=context):
result[record.id] = record.session_ids.filtered(lambda r: r.state == 'opened').user_id.name
result[record.id] = record.session_ids.filtered(lambda r: r.state == 'opened' and not r.rescue).user_id.name
return result
_columns = {
@@ -222,7 +222,7 @@ class pos_config(osv.osv):
'group_by' : True,
'pricelist_id': _default_pricelist,
'iface_invoicing': True,
'iface_print_auto': True,
'iface_print_auto': False,
'iface_print_skip_screen': True,
'stock_location_id': _get_default_location,
'company_id': _get_default_company,
@@ -238,6 +238,9 @@ class pos_config(osv.osv):
return {'value': {'stock_location_id': p_type.default_location_src_id.id}}
return False
def onchange_iface_print_via_proxy(self, cr, uid, ids, print_via_proxy, context=None):
return {'value': {'iface_print_auto': print_via_proxy}}
def set_active(self, cr, uid, ids, context=None):
return self.write(cr, uid, ids, {'state' : 'active'}, context=context)
+1 -1
View File
@@ -699,7 +699,7 @@
</group>
<group string="Hardware Proxy / PosBox" >
<field name="proxy_ip" />
<field name="iface_print_via_proxy" string="Receipt Printer"/>
<field name="iface_print_via_proxy" string="Receipt Printer" on_change="onchange_iface_print_via_proxy(iface_print_via_proxy)"/>
<field name="iface_scan_via_proxy" string="Barcode Scanner"/>
<field name="iface_electronic_scale" />
<field name="iface_cashdrawer" />
+2 -2
View File
@@ -11,7 +11,7 @@ class ProductTemplate(models.Model):
uos_id = fields.Many2one('product.uom', 'Unit of Sale',
help='Specify a unit of measure here if invoicing is made in another'
' unit of measure than inventory. Keep empty to use the default unit of measure.')
uos_coeff = fields.Float('Unit of Measure -> UOS Coeff', digits_compute=dp.get_precision('Product Unit of Measure'),
uos_coeff = fields.Float('Unit of Measure -> UOS Coeff', digits=dp.get_precision('Product Unit of Measure'),
help='Coefficient to convert default Unit of Measure to Unit of Sale'
' uos = uom * coeff')
@@ -29,7 +29,7 @@ class SaleOrderLine(models.Model):
def _compute_uos(self):
self.product_uos_qty = self.product_uom_qty * self.product_id.uos_coeff
product_uos_qty = fields.Float(string='Quantity', digits_compute=dp.get_precision('Product Unit of Measure'),
product_uos_qty = fields.Float(string='Quantity', digits=dp.get_precision('Product Unit of Measure'),
compute='_compute_uos', inverse='_set_uos', readonly=False)
product_uos = fields.Many2one('product.uom', string='Unit of Measure', required=True,
related='product_id.uos_id', readonly=True)
+3 -3
View File
@@ -102,7 +102,7 @@ class PurchaseOrder(models.Model):
"It's used to do the matching when you receive the "
"products as this reference is usually written on the "
"delivery order sent by your vendor.")
date_order = fields.Datetime('Order Date', required=True, states=READONLY_STATES, select=True, copy=False, default=fields.Date.context_today,\
date_order = fields.Datetime('Order Date', required=True, states=READONLY_STATES, select=True, copy=False, default=fields.Datetime.now(),\
help="Depicts the date where the Quotation should be validated and converted into a purchase order.")
date_approve = fields.Date('Approval Date', readonly=1, select=True, copy=False)
partner_id = fields.Many2one('res.partner', string='Vendor', required=True, states=READONLY_STATES, change_default=True, track_visibility='always')
@@ -441,13 +441,13 @@ class PurchaseOrderLine(models.Model):
line.qty_received = total
name = fields.Text(string='Description', required=True)
product_qty = fields.Float(string='Quantity', digits_compute=dp.get_precision('Product Unit of Measure'), required=True, default=1.0)
product_qty = fields.Float(string='Quantity', digits=dp.get_precision('Product Unit of Measure'), required=True, default=1.0)
date_planned = fields.Datetime(string='Scheduled Date', required=True, select=True)
taxes_id = fields.Many2many('account.tax', string='Taxes')
product_uom = fields.Many2one('product.uom', string='Product Unit of Measure', required=True)
product_id = fields.Many2one('product.product', string='Product', domain=[('purchase_ok', '=', True)], change_default=True, required=True)
move_ids = fields.One2many('stock.move', 'purchase_line_id', string='Reservation', readonly=True, ondelete='set null', copy=False)
price_unit = fields.Float(string='Unit Price', required=True, digits_compute=dp.get_precision('Product Price'))
price_unit = fields.Float(string='Unit Price', required=True, digits=dp.get_precision('Product Price'))
price_subtotal = fields.Monetary(compute='_compute_amount', string='Subtotal', store=True)
price_total = fields.Monetary(compute='_compute_amount', string='Total', store=True)
+1 -1
View File
@@ -200,7 +200,7 @@
<field name="qty_invoiced" invisible="not context.get('show_purchase', False)"/>
<field name="product_uom" groups="product.group_uom"/>
<field name="price_unit"/>
<field name="taxes_id" widget="many2many_tags" domain="[('type_tax_use','=','purchase')]"/>
<field name="taxes_id" widget="many2many_tags" domain="[('type_tax_use','=','purchase')]" context="{'default_type_tax_use': 'purchase'}"/>
<field name="price_subtotal" widget="monetary"/>
</tree>
<form string="Purchase Order Line">
+7 -6
View File
@@ -161,6 +161,7 @@ class SaleOrder(models.Model):
return 'sale.mt_order_sent'
return super(SaleOrder, self)._track_subtype(init_values)
@api.multi
@api.onchange('partner_shipping_id')
def onchange_partner_shipping_id(self):
"""
@@ -597,7 +598,7 @@ class SaleOrderLine(models.Model):
('to invoice', 'To Invoice'),
('no', 'Nothing to Invoice')
], string='Invoice Status', compute='_compute_invoice_status', store=True, readonly=True, default='no')
price_unit = fields.Float('Unit Price', required=True, digits_compute=dp.get_precision('Product Price'), default=0.0)
price_unit = fields.Float('Unit Price', required=True, digits=dp.get_precision('Product Price'), default=0.0)
price_subtotal = fields.Monetary(compute='_compute_amount', string='Subtotal', readonly=True, store=True)
price_tax = fields.Monetary(compute='_compute_amount', string='Taxes', readonly=True, store=True)
@@ -606,20 +607,20 @@ class SaleOrderLine(models.Model):
price_reduce = fields.Monetary(compute='_get_price_reduce', string='Price Reduce', readonly=True, store=True)
tax_id = fields.Many2many('account.tax', string='Taxes')
discount = fields.Float(string='Discount (%)', digits_compute=dp.get_precision('Discount'), default=0.0)
discount = fields.Float(string='Discount (%)', digits=dp.get_precision('Discount'), default=0.0)
product_id = fields.Many2one('product.product', string='Product', domain=[('sale_ok', '=', True)], change_default=True, ondelete='restrict', required=True)
product_uom_qty = fields.Float(string='Quantity', digits_compute=dp.get_precision('Product Unit of Measure'), required=True, default=1.0)
product_uom_qty = fields.Float(string='Quantity', digits=dp.get_precision('Product Unit of Measure'), required=True, default=1.0)
product_uom = fields.Many2one('product.uom', string='Unit of Measure', required=True)
qty_delivered_updateable = fields.Boolean(compute='_compute_qty_delivered_updateable', string='Can Edit Delivered', readonly=True, default=True)
qty_delivered = fields.Float(string='Delivered', copy=False, digits_compute=dp.get_precision('Product Unit of Measure'), default=0.0)
qty_delivered = fields.Float(string='Delivered', copy=False, digits=dp.get_precision('Product Unit of Measure'), default=0.0)
qty_to_invoice = fields.Float(
compute='_get_to_invoice_qty', string='To Invoice', store=True, readonly=True,
digits_compute=dp.get_precision('Product Unit of Measure'), default=0.0)
digits=dp.get_precision('Product Unit of Measure'), default=0.0)
qty_invoiced = fields.Float(
compute='_get_invoice_qty', string='Invoiced', store=True, readonly=True,
digits_compute=dp.get_precision('Product Unit of Measure'), default=0.0)
digits=dp.get_precision('Product Unit of Measure'), default=0.0)
salesman_id = fields.Many2one(related='order_id.user_id', store=True, string='Salesperson', readonly=True)
currency_id = fields.Many2one(related='order_id.currency_id', store=True, string='Currency', readonly=True)
+1 -1
View File
@@ -61,7 +61,7 @@
<th>Description</th>
<th class="text-right">Quantity</th>
<th class="text-right">Unit Price</th>
<th t-if="display_discount" class="text-right"><span groups="sale.group_discount_per_so_line">Disc.(%)</span></th>
<th t-if="display_discount" class="text-right" groups="sale.group_discount_per_so_line">Disc.(%)</th>
<th class="text-right">Taxes</th>
<th class="text-right">Price</th>
</tr>
@@ -133,8 +133,7 @@ class stock_history(osv.osv):
serial_number
FROM
((SELECT
stock_move.id::text || '-' || quant.id::text AS id,
quant.id AS quant_id,
stock_move.id AS id,
stock_move.id AS move_id,
dest_location.id AS location_id,
dest_location.company_id AS company_id,
@@ -148,19 +147,19 @@ class stock_history(osv.osv):
stock_production_lot.name AS serial_number
FROM
stock_quant as quant
LEFT JOIN
JOIN
stock_quant_move_rel ON stock_quant_move_rel.quant_id = quant.id
LEFT JOIN
JOIN
stock_move ON stock_move.id = stock_quant_move_rel.move_id
LEFT JOIN
stock_production_lot ON stock_production_lot.id = quant.lot_id
LEFT JOIN
JOIN
stock_location dest_location ON stock_move.location_dest_id = dest_location.id
LEFT JOIN
JOIN
stock_location source_location ON stock_move.location_id = source_location.id
LEFT JOIN
JOIN
product_product ON product_product.id = stock_move.product_id
LEFT JOIN
JOIN
product_template ON product_template.id = product_product.product_tmpl_id
WHERE quant.qty>0 AND stock_move.state = 'done' AND dest_location.usage in ('internal', 'transit')
AND (
@@ -168,10 +167,9 @@ class stock_history(osv.osv):
(source_location.company_id is not null and dest_location.company_id is null) or
source_location.company_id != dest_location.company_id or
source_location.usage not in ('internal', 'transit'))
) UNION
) UNION ALL
(SELECT
'-' || stock_move.id::text || '-' || quant.id::text AS id,
quant.id AS quant_id,
(-1) * stock_move.id AS id,
stock_move.id AS move_id,
source_location.id AS location_id,
source_location.company_id AS company_id,
@@ -185,19 +183,19 @@ class stock_history(osv.osv):
stock_production_lot.name AS serial_number
FROM
stock_quant as quant
LEFT JOIN
JOIN
stock_quant_move_rel ON stock_quant_move_rel.quant_id = quant.id
LEFT JOIN
JOIN
stock_move ON stock_move.id = stock_quant_move_rel.move_id
LEFT JOIN
stock_production_lot ON stock_production_lot.id = quant.lot_id
LEFT JOIN
JOIN
stock_location source_location ON stock_move.location_id = source_location.id
LEFT JOIN
JOIN
stock_location dest_location ON stock_move.location_dest_id = dest_location.id
LEFT JOIN
JOIN
product_product ON product_product.id = stock_move.product_id
LEFT JOIN
JOIN
product_template ON product_template.id = product_product.product_tmpl_id
WHERE quant.qty>0 AND stock_move.state = 'done' AND source_location.usage in ('internal', 'transit')
AND (
+4 -2
View File
@@ -50,12 +50,14 @@ if(!$('.js_surveyresult').length) {
//initialize discreteBar Chart
function init_bar_chart(){
return nv.models.discreteBarChart()
var chart = nv.models.discreteBarChart()
.x(function(d) { return d.text; })
.y(function(d) { return d.count; })
.staggerLabels(true)
.tooltips(false)
.showValues(true);
chart.tooltip.enabled(false);
return chart;
}
//initialize Pie Chart
@@ -954,24 +954,26 @@ var X2ManyListView = ListView.extend({
if (_.isEmpty(this.records.records)){
return true;
}
var fields = this.editor.form.fields;
var current_values = {};
_.each(this.editor.form.fields, function(field){
_.each(fields, function(field){
field._inhibit_on_change_flag = true;
field.no_rerender = true;
current_values[field.name] = field.get('value');
});
var valid = _.every(this.records.records, function(record){
_.each(self.editor.form.fields, function(field){
field.set_value(record.attributes[field.name]);
var valid = _.every(this.dataset.cache, function(record){
_.each(fields, function(field){
var value = record.values[field.name];
field.set_value(_.isArray(value) && _.isArray(value[0]) ? [COMMANDS.delete_all()].concat(value) : value);
});
return _.every(self.editor.form.fields, function(field){
return _.every(fields, function(field){
field.process_modifiers();
field._check_css_flags();
return field.is_valid();
});
});
_.each(this.editor.form.fields, function(field){
field.set('value', current_values[field.name]);
_.each(fields, function(field){
field.set('value', current_values[field.name], {silent: true});
field._inhibit_on_change_flag = false;
field.no_rerender = false;
});
@@ -738,9 +738,10 @@ var FieldPercentPie = common.AbstractField.extend({
.donut(true)
.showLegend(false)
.showLabels(false)
.tooltips(false)
.color(['#7C7BAD','#DDD'])
.donutRatio(0.62);
chart.tooltip.enabled(false);
d3.select(svg)
.datum([{'x': 'value', 'y': value}, {'x': 'complement', 'y': 100 - value}])
@@ -778,12 +779,13 @@ var FieldBarChart = common.AbstractField.extend({
.width(width)
.height(height)
.margin({top: 0, right: 0, bottom: 0, left: 0})
.tooltips(false)
.showValues(false)
.transition(350)
.showXAxis(false)
.showYAxis(false);
chart.tooltip.enabled(false);
d3.select(svg)
.datum([{key: 'values', values: value}])
.transition()
+16 -12
View File
@@ -405,23 +405,27 @@
}
.oe_overlay .oe_overlay_options {
position: absolute;
left: 50% !important;
text-align: center;
top: -20px;
width: 100%;
top: -35px;
z-index: 1002;
}
.oe_overlay .oe_overlay_options > .btn-group {
left: -50%;
white-space: nowrap;
display: -ms-flexbox;
display: -moz-box;
display: -webkit-box;
display: -webkit-flex;
display: flex;
-moz-justify-content: center;
-webkit-justify-content: center;
justify-content: center;
}
.oe_overlay .oe_overlay_options > .btn-group > a {
.oe_overlay .oe_overlay_options > .btn-group > * {
cursor: pointer;
display: inline-block;
float: none;
margin: 0 -3px;
}
.oe_overlay .oe_overlay_options > .btn-group > div {
float: left;
-ms-flex: 0 0 auto;
-moz-flex: 0 0 auto;
-webkit-box-flex: 0;
-webkit-flex: 0 0 auto;
flex: 0 0 auto;
}
.oe_overlay .oe_overlay_options .btn, .oe_overlay .oe_overlay_options a {
cursor: pointer;
+16 -11
View File
@@ -311,20 +311,25 @@
.oe_overlay_options
position: absolute
left: 50% !important
text-align: center
top: -20px
width: 100%
top: -35px
z-index: 1002
> .btn-group
left: -50%
white-space: nowrap
> a
display: -ms-flexbox
display: -moz-box
display: -webkit-box
display: -webkit-flex
display: flex
-moz-justify-content: center
-webkit-justify-content: center
justify-content: center
> *
cursor: pointer
display: inline-block
float: none
margin: 0 -3px
> div
float: left
-ms-flex: 0 0 auto
-moz-flex: 0 0 auto
-webkit-box-flex: 0
-webkit-flex: 0 0 auto
flex: 0 0 auto
.btn, a
cursor: pointer
.dropdown
+9 -11
View File
@@ -33,18 +33,16 @@ class view(osv.osv):
def _view_obj(self, cr, uid, view_id, context=None):
if isinstance(view_id, basestring):
try:
return self.pool['ir.model.data'].xmlid_to_object(
cr, uid, view_id, raise_if_not_found=True, context=context
)
except:
# Try to fallback on key instead of xml_id
if 'website_id' in context:
domain = [('key', '=', view_id), '|', ('website_id', '=', False), ('website_id', '=', context.get('website_id'))]
rec_id = self.search(cr, uid, domain, order='website_id', context=context)
else:
rec_id = self.search(cr, uid, [('key', '=', view_id)], context=context)
if rec_id:
_logger.info("Could not find view with `xml_id' '%s', fallback on `key'" % (view_id))
return self.browse(cr, uid, rec_id, context=context)[0]
else:
raise
if rec_id:
return self.browse(cr, uid, rec_id, context=context)[0]
else:
return self.pool['ir.model.data'].xmlid_to_object(
cr, uid, view_id, raise_if_not_found=True, context=context)
elif isinstance(view_id, (int, long)):
return self.browse(cr, uid, view_id, context=context)
@@ -228,7 +228,7 @@ options.registry.gallery = options.Class.extend({
var $container = this.$target.find(".container:first");
var editor = new widget.MediaDialog(this.$target.closest('.o_editable'), null, {select_images: true});
editor.appendTo(document.body);
var index = Math.max(_.map(this.$target.find("img").get(), function (img) { return img.dataset.index | 0; })) + 1;
var index = Math.max(0, _.max(_.map(this.$target.find("img").get(), function (img) { return img.dataset.index | 0; })) + 1);
editor.on('saved', this, function (attachments) {
for (var i = 0 ; i < attachments.length; i++) {
var img = $('<img class="img img-responsive mb8 mt8"/>')
@@ -335,7 +335,6 @@ options.registry.gallery_img = options.Class.extend({
if (type !== "click") return;
var $parent = this.$target.closest("section");
this.buildingBlock.create_overlay($parent);
var editor = $parent.data('snippet-editor').styles.gallery;
var imgs = $parent.find('img').get();
imgs.sort(function (a,b) { return $(a).data('index')-$(b).data('index'); });
+2 -2
View File
@@ -68,8 +68,8 @@
</a>
</li>
<li t-if="submenu.child_id" t-attf-class="dropdown #{
(submenu.url and submenu.url != '/' and any([request.httprequest.path == child.url.replace('/page/website.', '/page/') for child in submenu.child_id]) or
request.httprequest.path == submenu.url.replace('/page/website.', '/page/')) and 'active'
(submenu.url and submenu.url != '/' and any([request.httprequest.path == child.url.replace('/page/website.', '/page/') for child in submenu.child_id if child.url]) or
(submenu.url and request.httprequest.path == submenu.url.replace('/page/website.', '/page/'))) and 'active'
}">
<a class="dropdown-toggle" data-toggle="dropdown" href="#">
<span t-field="submenu.name"/> <span class="caret" t-ignore="true"></span>
+1
View File
@@ -157,6 +157,7 @@ class WebsiteBlog(http.Controller):
values = {
'blog': blog,
'blogs': blogs,
'main_object': blog,
'tags': all_tags,
'active_tag_ids': active_tag_ids,
'tags_list' : tags_list,
+2 -2
View File
@@ -245,10 +245,10 @@ class BlogPost(osv.Model):
self._check_for_publication(cr, uid, ids, vals, context=context)
return result
def get_access_action(self, cr, uid, id, context=None):
def get_access_action(self, cr, uid, ids, context=None):
""" Override method that generated the link to access the document. Instead
of the classic form view, redirect to the post on the website directly """
post = self.browse(cr, uid, id, context=context)
post = self.browse(cr, uid, ids[0], context=context)
return {
'type': 'ir.actions.act_url',
'url': '/blog/%s/post/%s' % (post.blog_id.id, post.id),
+2 -1
View File
@@ -704,10 +704,11 @@ class Post(models.Model):
self._cr.execute("""UPDATE forum_post SET views = views+1 WHERE id IN %s""", (self._ids,))
return True
@api.one
@api.multi
def get_access_action(self):
""" Override method that generated the link to access the document. Instead
of the classic form view, redirect to the post on the website directly """
self.ensure_one()
return {
'type': 'ir.actions.act_url',
'url': '/forum/%s/question/%s' % (self.forum_id.id, self.id),
@@ -7,10 +7,13 @@ from openerp import api, models
class sale_order(models.Model):
_inherit = 'sale.order'
@api.one
@api.multi
def get_access_action(self):
""" Override method that generated the link to access the document. Instead
of the classic form view, redirect to the online quote if exists. """
self.ensure_one()
if self.state in ['draft', 'cancel']:
return super(sale_order, self).get_access_action()
return {
'type': 'ir.actions.act_url',
'url': '/my/orders/%s' % self.id,
@@ -8,10 +8,11 @@ class Issue(models.Model):
_name = "project.issue"
_inherit = ['project.issue']
@api.one
@api.multi
def get_access_action(self):
""" Override method that generated the link to access the document. Instead
of the classic form view, redirect to the post on the website directly """
self.ensure_one()
return {
'type': 'ir.actions.act_url',
'url': '/my/issues/%s' % self.id,
+6
View File
@@ -196,6 +196,12 @@ class sale_quote(http.Controller):
'callback_eval': "self.env['sale.order']._confirm_online_quote(self.sale_order_id.id, self)"
}, context=context)
tx = transaction_obj.browse(cr, SUPERUSER_ID, tx_id, context=context)
# update quotation
request.registry['sale.order'].write(
cr, SUPERUSER_ID, [order.id], {
'payment_acquirer_id': acquirer_id,
'payment_tx_id': tx_id
}, context=context)
# confirm the quotation
if tx.acquirer_id.auto_confirm == 'at_pay_now':
+5 -5
View File
@@ -253,17 +253,17 @@ class sale_order(osv.osv):
products += line.product_id.product_tmpl_id.recommended_products(context=context)
return products
def get_access_action(self, cr, uid, id, context=None):
def get_access_action(self, cr, uid, ids, context=None):
""" Override method that generated the link to access the document. Instead
of the classic form view, redirect to the online quote if exists. """
quote = self.browse(cr, uid, id, context=context)
quote = self.browse(cr, uid, ids[0], context=context)
if not quote.template_id:
return super(sale_order, self).get_access_action(cr, uid, id, context=context)
return super(sale_order, self).get_access_action(cr, uid, ids, context=context)
return {
'type': 'ir.actions.act_url',
'url': '/quote/%s' % id,
'url': '/quote/%s' % quote.id,
'target': 'self',
'res_id': id,
'res_id': quote.id,
}
def _confirm_online_quote(self, cr, uid, order_id, tx, context=None):
+2 -3
View File
@@ -772,15 +772,14 @@
</div>
</td>
<td class="text-center" id="td-price" name="price">
<del t-attf-class="text-danger #{compute_currency(line.product_id.lst_price) - line.price_reduce &lt; 1 and 'hidden' or ''}" style="text-decoration: line-through; white-space: nowrap;"
<del t-attf-class="text-danger #{'' if (compute_currency(line.product_id.lst_price) - line.price_unit) &gt; 0.1 else 'hidden'}" style="text-decoration: line-through; white-space: nowrap;"
t-field="line.product_id.lst_price" t-field-options='{
"widget": "monetary",
"from_currency": "line.company_id.currency_id",
"display_currency": "website.currency_id"
}'/>
<span t-field="line.price_reduce" style="white-space: nowrap;" t-field-options='{
<span t-field="line.price_unit" style="white-space: nowrap;" t-field-options='{
"widget": "monetary",
"from_currency": "website_sale_order.pricelist_id.currency_id",
"display_currency": "website.currency_id"
}'/>
</td>
@@ -38,7 +38,7 @@
<span class="badge" t-field="delivery.price"
t-field-options='{
"widget": "monetary",
"from_currency": "website.currency_id",
"from_currency": "delivery.product_id.company_id.currency_id",
"display_currency": "website_sale_order.currency_id"
}'/>
<div t-field="delivery.website_description" class="text-muted"/>
+1 -1
View File
@@ -149,7 +149,7 @@ class ir_attachment(osv.osv):
r = os.path.getsize(full_path)
else:
r = open(full_path,'rb').read().encode('base64')
except IOError:
except (IOError, OSError):
_logger.info("_read_file reading %s", full_path, exc_info=True)
return r
+5
View File
@@ -212,6 +212,11 @@ class lang(osv.osv):
if isinstance(ids, (int, long)):
ids = [ids]
if 'code' in vals:
for rec in self.browse(cr, uid, ids, context):
if rec.code != vals['code']:
raise UserError(_("Language code cannot be modified."))
if vals.get('active') == False:
users = self.pool.get('res.users')
for current_id in ids:
+14 -1
View File
@@ -263,7 +263,20 @@ class TestAPI(common.TransactionCase):
@mute_logger('openerp.models')
def test_60_cache(self):
""" Check the record cache behavior """
partners = self.env['res.partner'].search([('child_ids', '!=', False)])
Partners = self.env['res.partner']
pids = []
data = {
'partner One': ['Partner One - One', 'Partner One - Two'],
'Partner Two': ['Partner Two - One'],
'Partner Three': ['Partner Three - One'],
}
for p in data:
pids.append(Partners.create({
'name': p,
'child_ids': [(0, 0, {'name': c}) for c in data[p]],
}).id)
partners = Partners.search([('id', 'in', pids)])
partner1, partner2 = partners[0], partners[1]
children1, children2 = partner1.child_ids, partner2.child_ids
self.assertTrue(children1)
@@ -1,3 +1,4 @@
"id","name","model_id:id","group_id:id","perm_read","perm_write","perm_create","perm_unlink"
access_test_inherit_mother,access_test_inherit_mother,model_test_inherit_mother,,1,1,1,1
access_test_inherit_daughter,access_test_inherit_daughter,model_test_inherit_daughter,,1,1,1,1
access_test_inherit_property,access_test_inherit_property,model_test_inherit_property,,1,1,1,1
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_test_inherit_mother access_test_inherit_mother model_test_inherit_mother 1 1 1 1
3 access_test_inherit_daughter access_test_inherit_daughter model_test_inherit_daughter 1 1 1 1
4 access_test_inherit_property access_test_inherit_property model_test_inherit_property 1 1 1 1
+32
View File
@@ -78,3 +78,35 @@ class res_partner(models.Model):
# define a one2many field based on the inherited field partner_id
daughter_ids = fields.One2many('test.inherit.daughter', 'partner_id')
# Check the overriding of property fields by non-property fields.
# Contribution by Adrien Peiffer (ACSONE).
class test_inherit_property(models.Model):
_name = 'test.inherit.property'
_columns = {
'name': osv.fields.char('Name', required=True),
'property_foo': osv.fields.property(string='Foo', type='integer'),
'property_bar': osv.fields.property(string='Bar', type='integer'),
}
class test_inherit_property(models.Model):
_inherit = 'test.inherit.property'
@api.multi
def _get_foo(self, name, arg):
return dict.fromkeys(self.ids, 42)
_columns = {
# override property_foo with an old-api function field
'property_foo': osv.fields.function(_get_foo, type='integer', string='Foo'),
}
# override property_bar with a new-api computed field
property_bar = fields.Integer(compute='_compute_bar', company_dependent=False)
@api.multi
def _compute_bar(self):
for record in self:
record.property_bar = 42
@@ -76,3 +76,20 @@ class test_inherits(common.TransactionCase):
# search the partner from the daughter record
partners = self.env['res.partner'].search([('daughter_ids', 'in', daughter.ids)])
self.assertIn(partner_demo, partners)
class test_override_property(common.TransactionCase):
def test_override_with_function_field(self):
""" test overriding a property field by a function field """
record = self.env['test.inherit.property'].create({'name': "Stuff"})
# record.property_foo is not a property field
self.assertEqual(record.property_foo, 42)
self.assertFalse(type(record).property_foo.company_dependent)
def test_override_with_computed_field(self):
""" test overriding a property field by a computed field """
record = self.env['test.inherit.property'].create({'name': "Stuff"})
# record.property_bar is not a property field
self.assertEqual(record.property_bar, 42)
self.assertFalse(type(record).property_bar.company_dependent)
+2 -2
View File
@@ -1607,14 +1607,14 @@ class BaseModel(object):
'context': context,
}
def get_access_action(self, cr, uid, id, context=None):
def get_access_action(self, cr, uid, ids, context=None):
""" Return an action to open the document. This method is meant to be
overridden in addons that want to give specific access to the document.
By default it opens the formview of the document.
:param int id: id of the document to open
"""
return self.get_formview_action(cr, uid, id, context=context)
return self.get_formview_action(cr, uid, ids[0], context=context)
def _view_look_dom_arch(self, cr, uid, node, view_id, context=None):
return self.pool['ir.ui.view'].postprocess_and_fields(
+1
View File
@@ -1430,6 +1430,7 @@ class function(_column):
def to_field_args(self):
args = super(function, self).to_field_args()
args['store'] = bool(self.store)
args['company_dependent'] = False
if self._type in ('float',):
args['digits'] = self._digits_compute or self._digits
elif self._type in ('binary',):
+1 -1
View File
@@ -466,7 +466,7 @@ def html_keep_url(text):
""" Transform the url into clickable link with <a/> tag """
idx = 0
final = ''
link_tags = re.compile(r'(ftp|http|https):\/\/(\w+:{0,1}\w*@)?(\S+)(:[0-9]+)?(\/|\/([\w#!:.?+=&%@!\-\/]))?')
link_tags = re.compile(r"""(?<!["'])((ftp|http|https):\/\/(\w+:{0,1}\w*@)?([^\s"':]+)(:[0-9]+)?(\/|\/([^\s"']))?)(?![^\s"']*["'])""")
for item in re.finditer(link_tags, text):
final += text[idx:item.start()]
final += '<a href="%s" target="_blank">%s</a>' % (item.group(0), item.group(0))
+6 -1
View File
@@ -52,7 +52,12 @@ class WorkflowInstance(object):
cr = self.session.cr
cr.execute("select * from wkf_workitem where inst_id=%s", (self.instance['id'],))
stack = []
for work_item_values in cr.dictfetchall():
for i, work_item_values in enumerate(cr.dictfetchall()):
if i > 0:
# test if previous workitem has already processed this one
cr.execute("select id from wkf_workitem where id=%s", (work_item_values['id'],))
if not cr.fetchone():
continue
wi = WorkflowItem(self.session, self.record, work_item_values)
wi.process(signal=signal, force_running=force_running, stack=stack)
# An action is returned