@api.model
def fields_view_get(self, view_id=None, view_type=False, toolbar=False, submenu=False):
context = self._context
+
+ def get_view_id(xid, name):
+ try:
+ return self.env['ir.model.data'].xmlid_to_res_id('account.' + xid, raise_if_not_found=True)
+ except ValueError:
+ try:
+ return self.env['ir.ui.view'].search([('name', '=', name)], limit=1).id
+ except Exception:
+ return False # view not found
+
if context.get('active_model') == 'res.partner' and context.get('active_ids'):
partner = self.env['res.partner'].browse(context['active_ids'])[0]
if not view_type:
- view_id = self.env['ir.ui.view'].search([('name', '=', 'account.invoice.tree')]).id
+ view_id = get_view_id('invoice_tree', 'account.invoice.tree')
view_type = 'tree'
elif view_type == 'form':
if partner.supplier and not partner.customer:
- view_id = self.env['ir.ui.view'].search([('name', '=', 'account.invoice.supplier.form')]).id
+ view_id = get_view_id('invoice_supplier_form', 'account.invoice.supplier.form')
elif partner.customer and not partner.supplier:
- view_id = self.env['ir.ui.view'].search([('name', '=', 'account.invoice.form')]).id
+ view_id = get_view_id('invoice_form', 'account.invoice.form')
res = super(account_invoice, self).fields_view_get(view_id=view_id, view_type=view_type, toolbar=toolbar, submenu=submenu)
journal_data = journal_obj.browse(cr, uid, context['journal_id'], context=context)
account = total > 0 and journal_data.default_credit_account_id or journal_data.default_debit_account_id
#map the account using the fiscal position of the partner, if needed
- part = data.get('partner_id') and partner_obj.browse(cr, uid, data['partner_id'], context=context) or False
- if account and data.get('partner_id'):
+ if isinstance(data.get('partner_id'), (int, long)):
+ part = partner_obj.browse(cr, uid, data['partner_id'], context=context)
+ elif isinstance(data.get('partner_id'), (tuple, list)):
+ part = partner_obj.browse(cr, uid, data['partner_id'][0], context=context)
+ else:
+ part = False
+ if account and part:
account = fiscal_pos_obj.map_account(cr, uid, part and part.property_account_position or False, account.id)
account = account_obj.browse(cr, uid, account, context=context)
data['account_id'] = account and account.id or False
move[line.move_id.id] = True
move_line_ids = []
if move:
- move_line_ids = self.pool.get('account.move.line').search(cr, uid, [('journal_id','in',move.keys())], context=context)
+ move_line_ids = self.pool.get('account.move.line').search(cr, uid, [('move_id','in',move.keys())], context=context)
return move_line_ids
def map_tax(self, taxes):
result = self.env['account.tax'].browse()
for tax in taxes:
+ tax_count = 0
for t in self.tax_ids:
if t.tax_src_id == tax:
+ tax_count += 1
if t.tax_dest_id:
result |= t.tax_dest_id
- else:
+ if not tax_count:
result |= tax
return result
self.account_ids = []
self.localcontext.update( {
'time': time,
- 'lines': self.lines,
- 'sum_debit': self._sum_debit,
- 'sum_credit': self._sum_credit,
- 'sum_litige': self._sum_litige,
'get_fiscalyear': self._get_fiscalyear,
'get_journal': self._get_journal,
'get_filter': self._get_filter,
"WHERE a.type IN %s " \
"AND a.active", (self.ACCOUNT_TYPE,))
self.account_ids = [a for (a,) in self.cr.fetchall()]
- return super(partner_balance, self).set_context(objects, data, ids, report_type=report_type)
+ res = super(partner_balance, self).set_context(objects, data, ids, report_type=report_type)
+ lines = self.lines()
+ sum_debit = sum_credit = sum_litige = 0
+ for line in filter(lambda x: x['type'] == 3, lines):
+ sum_debit += line['debit'] or 0
+ sum_credit += line['credit'] or 0
+ sum_litige += line['enlitige'] or 0
+ self.localcontext.update({
+ 'lines': lambda: lines,
+ 'sum_debit': lambda: sum_debit,
+ 'sum_credit': lambda: sum_credit,
+ 'sum_litige': lambda: sum_litige,
+ })
+ return res
def lines(self):
move_state = ['draft','posted']
i = i + 1
return completearray
- def _sum_debit(self):
- move_state = ['draft','posted']
- if self.target_move == 'posted':
- move_state = ['posted']
-
- if not self.ids:
- return 0.0
- self.cr.execute(
- "SELECT sum(debit) " \
- "FROM account_move_line AS l " \
- "JOIN account_move am ON (am.id = l.move_id)" \
- "WHERE l.account_id IN %s" \
- "AND am.state IN %s" \
- "AND " + self.query + "",
- (tuple(self.account_ids), tuple(move_state)))
- temp_res = float(self.cr.fetchone()[0] or 0.0)
- return temp_res
-
- def _sum_credit(self):
- move_state = ['draft','posted']
- if self.target_move == 'posted':
- move_state = ['posted']
-
- if not self.ids:
- return 0.0
- self.cr.execute(
- "SELECT sum(credit) " \
- "FROM account_move_line AS l " \
- "JOIN account_move am ON (am.id = l.move_id)" \
- "WHERE l.account_id IN %s" \
- "AND am.state IN %s" \
- "AND " + self.query + "",
- (tuple(self.account_ids), tuple(move_state)))
- temp_res = float(self.cr.fetchone()[0] or 0.0)
- return temp_res
-
- def _sum_litige(self):
- #gives the total of move lines with blocked boolean set to TRUE for the report selection
- move_state = ['draft','posted']
- if self.target_move == 'posted':
- move_state = ['posted']
-
- if not self.ids:
- return 0.0
- self.cr.execute(
- "SELECT sum(debit-credit) " \
- "FROM account_move_line AS l " \
- "JOIN account_move am ON (am.id = l.move_id)" \
- "WHERE l.account_id IN %s" \
- "AND am.state IN %s" \
- "AND " + self.query + " " \
- "AND l.blocked=TRUE ",
- (tuple(self.account_ids), tuple(move_state), ))
- temp_res = float(self.cr.fetchone()[0] or 0.0)
- return temp_res
-
def _get_partners(self):
if self.result_selection == 'customer':
<field name="domain_force">['|',('company_id','=',False),('company_id','child_of',[user.company_id.id])]</field>
</record>
+ <record id="analytic_entry_analysis_comp_rule" model="ir.rule">
+ <field name="name">Analytic Entries Analysis multi-company</field>
+ <field name="model_id" ref="model_analytic_entries_report"/>
+ <field name="global" eval="True"/>
+ <field name="domain_force">['|',('company_id','=',False),('company_id','child_of',[user.company_id.id])]</field>
+ </record>
+
<record id="account_fiscal_position_comp_rule" model="ir.rule">
<field name="name">Account fiscal Mapping company rule</field>
<field name="model_id" ref="model_account_fiscal_position"/>
if inv.type in ('in_invoice', 'in_refund'):
ref = inv.reference
else:
- ref = self._convert_ref(inv.number)
+ ref = inv.number
obj_move_line = acct_ins_obj.browse(cr, uid, il['analytics_id'], context=context)
ctx = context.copy()
ctx.update({'date': inv.date_invoice})
'iban': fields.related('acc_number', string='IBAN', size=34, readonly=True, help="International Bank Account Number", type="char"),
}
_constraints = [
- (check_iban, _construct_constraint_msg, ["iban"]),
+ (check_iban, _construct_constraint_msg, ["iban", "acc_number", "state"]),
(_check_bank, '\nPlease define BIC/Swift code on bank for bank type IBAN Account to make valid payments', ['bic'])
]
+@charset "UTF-8";
.openerp .oe_dashboard_layout_1 .oe_dashboard_column.index_0 {
width: 100%;
}
+@charset "utf-8"
+
@mixin radius($radius: 5px)
-moz-border-radius: $radius
-webkit-border-radius: $radius
},
start_polling: function(){
if(!this.activated){
- setTimeout(this.poll(), 1);
+ this.poll();
this.stop = false;
}
},
// singleton
bus.bus = new bus.Bus();
return bus;
-})();
\ No newline at end of file
+})();
return user_obj.browse(self.cr, self.uid, ids)
def _journal_ids(self, form, user_id):
+ if isinstance(user_id, (int, long)):
+ user_id = [user_id]
line_obj = self.pool['account.analytic.line']
journal_obj = self.pool['account.analytic.journal']
line_ids=line_obj.search(self.cr, self.uid, [
('date', '>=', form['date_from']),
('date', '<=', form['date_to']),
('journal_id', 'in', form['journal_ids'][0][2]),
- ('user_id', '=', user_id),
+ ('user_id', 'in', user_id),
])
ids=list(set([b.journal_id.id for b in line_obj.browse(self.cr, self.uid, line_ids)]))
return journal_obj.browse(self.cr, self.uid, ids)
**Credits:** Sistheo, Zeekom, CrysaLEAD, Akretion and Camptocamp.
""",
- 'depends': ['base_iban', 'account', 'account_chart', 'base_vat', 'l10n_fr_rib'],
+ 'depends': ['base_iban', 'account', 'account_chart', 'base_vat'],
'data': [
'views/report_l10nfrbilan.xml',
'views/report_l10nfrresultat.xml',
</p>
</div>
</div>
+ <h3>Actif</h3>
<table class="table table-condensed">
<thead>
<tr>
</td>
</tr>
</table>
+
+ <h3>Passif</h3>
+ <table class="table table-condensed">
+ <tbody>
+ <tr>
+ <td><strong>CAPITAUX PROPRES</strong></td>
+ <td></td>
+ </tr>
+ <tr>
+ <td>Capital [dont versé...]</td>
+ <td><span t-esc="bpvar1" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/></td>
+ </tr>
+ <tr>
+ <td>Primes d'émission, de fusion, d'apport</td>
+ <td><span t-esc="bpvar2" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/></td>
+ </tr>
+ <tr>
+ <td>Écarts de réévaluation</td>
+ <td><span t-esc="bpvar3" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/></td>
+ </tr>
+ <tr>
+ <td>Écart d'équivalence</td>
+ <td><span t-esc="bpvar4" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/></td>
+ </tr>
+ <tr>
+ <td><strong>RÉSERVES</strong></td>
+ <td></td>
+ </tr>
+ <tr>
+ <td>Réserve légale</td>
+ <td><span t-esc="bpvar5" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/></td>
+ </tr>
+ <tr>
+ <td>Réserves statutaires ou contractuelles</td>
+ <td><span t-esc="bpvar6" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/></td>
+ </tr>
+ <tr>
+ <td>Réserves réglementées</td><td><span t-esc="bpvar7" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/></td></tr>
+ <tr>
+ <td>Autres réserves</td>
+ <td><span t-esc="bpvar8" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/></td>
+ </tr>
+ <tr>
+ <td>Report à nouveau</td>
+ <td><span t-esc="bpvar9" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/></td>
+ </tr>
+ <tr>
+ <td><strong>RÉSULTAT DE L'EXERCICE [bénéfice ou perte]</strong></td>
+ <td><span t-esc="bpvar10" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/></td>
+ </tr>
+ <tr>
+ <td>Subventions d'investissement</td>
+ <td><span t-esc="bpvar11" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/></td>
+ </tr>
+ <tr>
+ <td>Provisions réglementées</td>
+ <td><span t-esc="bpvar12" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/></td>
+ </tr>
+ <tr>
+ <td class="text-right"><strong>TOTAL I</strong></td>
+ <td><span t-esc="pt1" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/></td>
+ </tr>
+ <tr>
+ <td><strong>PROVISIONS</strong></td>
+ <td></td>
+ </tr>
+ <tr>
+ <td>Provisions pour risques</td>
+ <td><span t-esc="bpvar13" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/></td>
+ </tr>
+ <tr>
+ <td>Provisions pour charges</td>
+ <td><span t-esc="bpvar14" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/></td>
+ </tr>
+ <tr>
+ <td class="text-right"><strong>TOTAL II</strong></td>
+ <td><span t-esc="pt2" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/></td>
+ </tr>
+ <tr>
+ <td><strong>DETTES</strong></td>
+ <td></td>
+ </tr>
+ <tr>
+ <td>Emprunts obligataires convertibles</td>
+ <td><span t-esc="bpvar15" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/></td>
+ </tr>
+ <tr>
+ <td>Autres emprunts obligataires</td>
+ <td><span t-esc="bpvar16" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/></td>
+ </tr>
+ <tr>
+ <td>Emprunts et dettes auprès des établissements de crédit</td>
+ <td><span t-esc="bpvar17" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/></td>
+ </tr>
+ <tr>
+ <td>Emprunts et dettes financières diverses</td>
+ <td><span t-esc="bpvar18" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/></td>
+ </tr>
+ <tr>
+ <td>Avances et acomptes reçus sur commandes en cours</td>
+ <td><span t-esc="bpvar19" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/></td>
+ </tr>
+ <tr>
+ <td>Dettes fournisseurs et comptes rattachés </td>
+ <td><span t-esc="bpvar20" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/></td>
+ </tr>
+ <tr>
+ <td>Dettes fiscales et sociales</td>
+ <td><span t-esc="bpvar21" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/></td>
+ </tr>
+ <tr>
+ <td>Dettes sur immobilisations et comptes rattachés</td>
+ <td><span t-esc="bpvar22" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/></td>
+ </tr>
+ <tr>
+ <td>Autres dettes</td>
+ <td><span t-esc="bpvar23" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/></td>
+ </tr>
+ <tr>
+ <td>Instruments de trésorerie</td>
+ <td><span t-esc="bpvar24" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/></td>
+ </tr>
+ <tr>
+ <td>Produits constatés d'avance</td>
+ <td><span t-esc="bpvar25" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/></td>
+ </tr>
+ <tr>
+ <td class="text-right"><strong>TOTAL III</strong></td>
+ <td><span t-esc="pt3" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/></td>
+ </tr>
+ <tr>
+ <td>Écarts de conversion passif <font face="Times-Roman">( IV )</font></td>
+ <td><span t-esc="bpvar26" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/></td>
+ </tr>
+ <tr>
+ <td><strong>TOTAL GÉNÉRAL (I + II + III + IV)</strong></td>
+ <td><span t-esc="passif" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/></td>
+ </tr>
+ <tr>
+ <td>&nbsp;</td>
+ <td>&nbsp;</td>
+ </tr>
+ <tr>
+ <td><strong>ACTIF - PASSIF</strong></td>
+ <td><span t-esc="round(actif-passif,2)" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/></td>
+ </tr>
+ </tbody>
+ </table>
</div>
</t>
</t>
self.$el.toggleClass('oe_pad_fullscreen');
self.$el.find('.oe_pad_switch').toggleClass('fa-expand fa-compress');
self.view.$el.find('.oe_chatter').toggle();
+ $('#oe_main_menu_navbar').toggle();
});
this._configured_deferred.always(function() {
var configured = self.get('configured');
tx_ids = request.registry['payment.transaction'].search(cr, uid, [('reference', '=', reference)], context=context)
if tx_ids:
tx = request.registry['payment.transaction'].browse(cr, uid, tx_ids[0], context=context)
- paypal_urls = request.registry['payment.acquirer']._get_paypal_urls(cr, uid, tx and tx.acquirer_id and tx.acquirer_id.env or 'prod', context=context)
+ paypal_urls = request.registry['payment.acquirer']._get_paypal_urls(cr, uid, tx and tx.acquirer_id and tx.acquirer_id.environment or 'prod', context=context)
validate_url = paypal_urls['paypal_form_url']
urequest = urllib2.Request(validate_url, werkzeug.url_encode(new_post))
uopen = urllib2.urlopen(urequest)
pos_ids = pos_order_obj.search(self.cr, self.uid, [('date_order','>=',form['date_start'] + ' 00:00:00'),('date_order','<=',form['date_end'] + ' 23:59:59'),('state','in',['paid','invoiced','done']),('user_id','in',user_ids)])
for order in pos_order_obj.browse(self.cr, self.uid, pos_ids):
for line in order.lines:
- line_taxes = account_tax_obj.compute_all(self.cr, self.uid, line.product_id.taxes_id, line.price_unit, line.qty, product=line.product_id, partner=line.order_id.partner_id or False)
+ line_taxes = account_tax_obj.compute_all(self.cr, self.uid, line.product_id.taxes_id, line.price_unit * (1-(line.discount or 0.0)/100.0), line.qty, product=line.product_id, partner=line.order_id.partner_id or False)
for tax in line_taxes['taxes']:
taxes.setdefault(tax['id'], {'name': tax['name'], 'amount':0.0})
taxes[tax['id']]['amount'] += tax['amount']
<tr t-if="gettaxamount(data['form'])"><td colspan="2"><strong>Taxes</strong></td></tr>
<tr t-foreach="gettaxamount(data['form'])" t-as="tax">
<td><span t-esc="tax['name']"/></td>
- <td class="text_right">
+ <td class="text-right">
<strong t-esc="formatLang(tax['amount'], currency_obj = res_company.currency_id)"/>
</td>
</tr>
price = False
rule_id = False
for rule in items:
- if rule.min_quantity and qty<rule.min_quantity:
+ if 'uom' in context:
+ qty_in_product_uom = product_uom_obj._compute_qty(cr, uid, context['uom'], qty, product.uom_id.id or product.uos_id.id)
+ else:
+ qty_in_product_uom = qty
+ if rule.min_quantity and qty_in_product_uom<rule.min_quantity:
continue
if is_product_template:
if rule.product_tmpl_id and product.id != rule.product_tmpl_id.id:
price = price * (1.0+(rule.price_discount or 0.0))
if rule.price_round:
price = tools.float_round(price, precision_rounding=rule.price_round)
- price += (rule.price_surcharge or 0.0)
+ if context.get('uom'):
+ # compute price_surcharge based on reference uom
+ factor = product_uom_obj.browse(cr, uid, context.get('uom'), context=context).factor
+ else:
+ factor = 1.0
+ price += (rule.price_surcharge or 0.0) / factor
if rule.price_min_margin:
price = max(price, price_limit+rule.price_min_margin)
if rule.price_max_margin:
def need_procurement(self, cr, uid, ids, context=None):
return False
+ def _compute_uos_qty(self, cr, uid, ids, uom, qty, uos, context=None):
+ '''
+ Computes product's invoicing quantity in UoS from quantity in UoM.
+ Takes into account the
+ :param uom: Source unit
+ :param qty: Source quantity
+ :param uos: Target UoS unit.
+ '''
+ if not uom or not qty or not uos:
+ return qty
+ uom_obj = self.pool['product.uom']
+ product_id = ids[0] if isinstance(ids, (list, tuple)) else ids
+ product = self.browse(cr, uid, product_id, context=context)
+ if isinstance(uos, (int, long)):
+ uos = uom_obj.browse(cr, uid, uos, context=context)
+ if isinstance(uom, (int, long)):
+ uom = uom_obj.browse(cr, uid, uom, context=context)
+ if product.uos_id: # Product has UoS defined
+ # We cannot convert directly between units even if the units are of the same category
+ # as we need to apply the conversion coefficient which is valid only between quantities
+ # in product's default UoM/UoS
+ qty_default_uom = uom_obj._compute_qty_obj(cr, uid, uom, qty, product.uom_id) # qty in product's default UoM
+ qty_default_uos = qty_default_uom * product.uos_coeff
+ return uom_obj._compute_qty_obj(cr, uid, product.uos_id, qty_default_uos, uos)
+ else:
+ return uom_obj._compute_qty_obj(cr, uid, uom, qty, uos)
+
+
class product_packaging(osv.osv):
_name = "product.packaging"
-from . import test_uom
+from . import test_uom, test_pricelist
fast_suite = [
test_uom,
+ test_pricelist
]
--- /dev/null
+from openerp.tests.common import TransactionCase
+
+class TestPricelist(TransactionCase):
+ """Tests for unit of measure conversion"""
+
+ def setUp(self):
+ super(TestPricelist, self).setUp()
+ cr, uid, context = self.cr, self.uid, {}
+ self.ir_model_data = self.registry('ir.model.data')
+ self.product_product = self.registry('product.product')
+ self.product_pricelist = self.registry('product.pricelist')
+ self.uom = self.registry('product.uom')
+
+ self.usb_adapter_id = self.ir_model_data.get_object_reference(cr, uid, 'product', 'product_product_48')[1]
+ self.datacard_id = self.ir_model_data.get_object_reference(cr, uid, 'product', 'product_product_46')[1]
+ self.unit_id = self.ir_model_data.get_object_reference(cr, uid, 'product', 'product_uom_unit')[1]
+ self.dozen_id = self.ir_model_data.get_object_reference(cr, uid, 'product', 'product_uom_dozen')[1]
+
+ self.public_pricelist_id = self.ir_model_data.get_object_reference(cr, uid, 'product', 'list0')[1]
+ self.sale_pricelist_id = self.product_pricelist.create(cr, uid, {
+ 'name': 'Sale pricelist',
+ 'type': 'sale',
+ 'version_id': [(0, 0, {
+ 'name': 'v1.0',
+ 'items_id': [(0, 0, {
+ 'name': 'Discount 10%',
+ 'base': 1, # based on public price
+ 'price_discount': -0.1,
+ 'product_id': self.usb_adapter_id
+ }), (0, 0, {
+ 'name': 'Discount -0.5',
+ 'base': 1, # based on public price
+ 'price_surcharge': -0.5,
+ 'product_id': self.datacard_id
+ })]
+ })]
+ }, context=context)
+
+ def test_10_discount(self):
+ # Make sure the price using a pricelist is the same than without after
+ # applying the computation manually
+ cr, uid, context = self.cr, self.uid, {}
+
+ public_context = dict(context, pricelist=self.public_pricelist_id)
+ pricelist_context = dict(context, pricelist=self.sale_pricelist_id)
+
+ usb_adapter_without_pricelist = self.product_product.browse(cr, uid, self.usb_adapter_id, context=public_context)
+ usb_adapter_with_pricelist = self.product_product.browse(cr, uid, self.usb_adapter_id, context=pricelist_context)
+ self.assertEqual(usb_adapter_with_pricelist.price, usb_adapter_without_pricelist.price*0.9)
+
+ datacard_without_pricelist = self.product_product.browse(cr, uid, self.datacard_id, context=public_context)
+ datacard_with_pricelist = self.product_product.browse(cr, uid, self.datacard_id, context=pricelist_context)
+ self.assertEqual(datacard_with_pricelist.price, datacard_without_pricelist.price-0.5)
+
+ # Make sure that changing the unit of measure does not break the unit
+ # price (after converting)
+ unit_context = dict(context,
+ pricelist=self.sale_pricelist_id,
+ uom=self.unit_id)
+ dozen_context = dict(context,
+ pricelist=self.sale_pricelist_id,
+ uom=self.dozen_id)
+
+ usb_adapter_unit = self.product_product.browse(cr, uid, self.usb_adapter_id, context=unit_context)
+ usb_adapter_dozen = self.product_product.browse(cr, uid, self.usb_adapter_id, context=dozen_context)
+ self.assertAlmostEqual(usb_adapter_unit.price*12, usb_adapter_dozen.price)
+
+ datacard_unit = self.product_product.browse(cr, uid, self.datacard_id, context=unit_context)
+ datacard_dozen = self.product_product.browse(cr, uid, self.datacard_id, context=dozen_context)
+ self.assertAlmostEqual(datacard_unit.price*12, datacard_dozen.price)
fiscal_position=False, flag=False, context=None):
def get_real_price(res_dict, product_id, qty, uom, pricelist):
+ """Retrieve the price before applying the pricelist"""
item_obj = self.pool.get('product.pricelist.item')
price_type_obj = self.pool.get('product.price.type')
product_obj = self.pool.get('product.product')
factor = 1.0
if uom and uom != product.uom_id.id:
- product_uom_obj = self.pool.get('product.uom')
- uom_data = product_uom_obj.browse(cr, uid, product.uom_id.id)
- factor = uom_data.factor
+ # the unit price is in a different uom
+ factor = self.pool['product.uom']._compute_qty(cr, uid, uom, 1.0, product.uom_id.id)
return product_read[field_name] * factor
result=res['value']
pricelist_obj=self.pool.get('product.pricelist')
product_obj = self.pool.get('product.product')
- if product and pricelist:
+ if product and pricelist and self.pool.get('res.users').has_group(cr, uid, 'sale.group_discount_per_so_line'):
if result.get('price_unit',False):
price=result['price_unit']
else:
return res
-
+ uom = result.get('product_uom', uom)
product = product_obj.browse(cr, uid, product, context)
pricelist_context = dict(context, uom=uom, date=date_order)
list_price = pricelist_obj.price_rule_get(cr, uid, [pricelist],
<field name="date_planned"/>
<field name="company_id" groups="base.group_multi_company" widget="selection"/>
<field name="account_analytic_id" groups="purchase.group_analytic_accounting" domain="[('type','not in',('view','template'))]"/>
- <field name="product_qty" on_change="onchange_product_id(parent.pricelist_id,product_id,product_qty,product_uom,parent.partner_id,parent.date_order,parent.fiscal_position,date_planned,name,price_unit,parent.state,context)"/>
- <field name="product_uom" groups="product.group_uom" on_change="onchange_product_uom(parent.pricelist_id,product_id,product_qty,product_uom,parent.partner_id, parent.date_order,parent.fiscal_position,date_planned,name,price_unit,parent.state,context)"/>
+ <field name="product_qty" on_change="onchange_product_id(parent.pricelist_id,product_id,product_qty,product_uom,parent.partner_id,parent.date_order,parent.fiscal_position,date_planned,name,False,parent.state,context)"/>
+ <field name="product_uom" groups="product.group_uom" on_change="onchange_product_uom(parent.pricelist_id,product_id,product_qty,product_uom,parent.partner_id, parent.date_order,parent.fiscal_position,date_planned,name,False,parent.state,context)"/>
<field name="price_unit"/>
<field name="taxes_id" widget="many2many_tags" domain="[('parent_id','=',False),('type_tax_use','!=','sale')]"/>
<field name="price_subtotal"/>
<sheet>
<group>
<group>
- <field name="product_id" on_change="onchange_product_id(parent.pricelist_id,product_id,0,product_uom,parent.partner_id, parent.date_order,parent.fiscal_position,date_planned,name,price_unit,'draft',context)"/>
+ <field name="product_id" on_change="onchange_product_id(parent.pricelist_id,product_id,0,False,parent.partner_id, parent.date_order,parent.fiscal_position,date_planned,name,False,'draft',context)"/>
<label for="product_qty"/>
<div>
- <field name="product_qty" on_change="onchange_product_id(parent.pricelist_id,product_id,product_qty,product_uom,parent.partner_id,parent.date_order,parent.fiscal_position,date_planned,name,price_unit,'draft',context)" class="oe_inline"/>
- <field name="product_uom" groups="product.group_uom" on_change="onchange_product_uom(parent.pricelist_id,product_id,product_qty,product_uom,parent.partner_id, parent.date_order,parent.fiscal_position,date_planned,name,price_unit,'draft',context)" class="oe_inline"/>
+ <field name="product_qty" on_change="onchange_product_id(parent.pricelist_id,product_id,product_qty,product_uom,parent.partner_id,parent.date_order,parent.fiscal_position,date_planned,name,False,'draft',context)" class="oe_inline"/>
+ <field name="product_uom" groups="product.group_uom" on_change="onchange_product_uom(parent.pricelist_id,product_id,product_qty,product_uom,parent.partner_id, parent.date_order,parent.fiscal_position,date_planned,name,False,'draft',context)" class="oe_inline"/>
</div>
<field name="price_unit"/>
</group>
<field name="model">product.template</field>
<field name="inherit_id" ref="product.product_template_search_view"/>
<field name="arch" type="xml">
- <filter name="filter_to_sell" position="before">
- <filter name="filter_to_purchase" string="To Purchase" icon="terp-accessories-archiver+" domain="[('purchase_ok', '=', 1)]"/>
- </filter>
<filter name="filter_to_sell" position="after">
<filter name="filter_to_purchase" string="Can be Purchased" icon="terp-accessories-archiver+" domain="[('purchase_ok', '=', 1)]"/>
</filter>
<?xml version="1.0" encoding="utf-8"?>
<openerp>
<data>
-<template id="report_purchaseorder">
- <t t-call="report.html_container">
- <t t-foreach="docs" t-as="o">
- <t t-call="report.external_layout">
- <div class="page">
- <div class="oe_structure"/>
- <div class="row">
- <div class="col-xs-6">
- Shipping address :<br/>
- <div t-if="o.dest_address_id">
- <div t-field="o.dest_address_id"
- t-field-options='{"widget": "contact", "fields": ["address", "name", "phone", "fax"], "no_marker": true}'/>
- <p t-if="o.partner_id.vat">VAT: <span t-field="o.partner_id.vat"/></p>
- </div>
+<template id="report_purchaseorder_document">
+ <t t-call="report.external_layout">
+ <div class="page">
+ <div class="oe_structure"/>
+ <div class="row">
+ <div class="col-xs-6">
+ Shipping address :<br/>
+ <div t-if="o.dest_address_id">
+ <div t-field="o.dest_address_id"
+ t-field-options='{"widget": "contact", "fields": ["address", "name", "phone", "fax"], "no_marker": true}'/>
+ <p t-if="o.partner_id.vat">VAT: <span t-field="o.partner_id.vat"/></p>
+ </div>
- <div t-if="o.picking_type_id and o.picking_type_id.warehouse_id">
- <span t-field="o.picking_type_id.warehouse_id.name"/>
- <div t-field="o.picking_type_id.warehouse_id.partner_id"
- t-field-options='{"widget": "contact", "fields": ["address", "name", "phone", "fax"], "no_marker": true}'/>
- <p t-if="o.partner_id.vat">VAT: <span t-field="o.partner_id.vat"/></p>
- </div>
- </div>
- <div class="col-xs-5 col-xs-offset-1">
- <div t-field="o.partner_id"
- t-field-options='{"widget": "contact", "fields": ["address", "name", "phone", "fax"], "no_marker": true}'/>
- </div>
+ <div t-if="o.picking_type_id and o.picking_type_id.warehouse_id">
+ <span t-field="o.picking_type_id.warehouse_id.name"/>
+ <div t-field="o.picking_type_id.warehouse_id.partner_id"
+ t-field-options='{"widget": "contact", "fields": ["address", "name", "phone", "fax"], "no_marker": true}'/>
+ <p t-if="o.partner_id.vat">VAT: <span t-field="o.partner_id.vat"/></p>
</div>
+ </div>
+ <div class="col-xs-5 col-xs-offset-1">
+ <div t-field="o.partner_id"
+ t-field-options='{"widget": "contact", "fields": ["address", "name", "phone", "fax"], "no_marker": true}'/>
+ </div>
+ </div>
- <h2 t-if="o.state != 'draft'">Purchase Order Confirmation N°<span t-field="o.name"/></h2>
- <h2 t-if="o.state == 'draft'">Request for Quotation N°<span t-field="o.name"/></h2>
+ <h2 t-if="o.state != 'draft'">Purchase Order Confirmation N°<span t-field="o.name"/></h2>
+ <h2 t-if="o.state == 'draft'">Request for Quotation N°<span t-field="o.name"/></h2>
- <div class="row mt32 mb32">
- <div t-if="o.name" class="col-xs-3">
- <strong>Our Order Reference:</strong>
- <p t-field="o.name"/>
- </div>
- <div t-if="o.partner_ref" class="col-xs-3">
- <strong>Your Order Reference</strong>
- <p t-field="o.partner_ref"/>
- </div>
- <div t-if="o.date_order" class="col-xs-3">
- <strong>Order Date:</strong>
- <p t-field="o.date_order"/>
- </div>
- <div t-if="o.validator" class="col-xs-3">
- <strong>Validated By:</strong>
- <p t-field="o.validator"/>
- </div>
- </div>
+ <div class="row mt32 mb32">
+ <div t-if="o.name" class="col-xs-3">
+ <strong>Our Order Reference:</strong>
+ <p t-field="o.name"/>
+ </div>
+ <div t-if="o.partner_ref" class="col-xs-3">
+ <strong>Your Order Reference</strong>
+ <p t-field="o.partner_ref"/>
+ </div>
+ <div t-if="o.date_order" class="col-xs-3">
+ <strong>Order Date:</strong>
+ <p t-field="o.date_order"/>
+ </div>
+ <div t-if="o.validator" class="col-xs-3">
+ <strong>Validated By:</strong>
+ <p t-field="o.validator"/>
+ </div>
+ </div>
+ <table class="table table-condensed">
+ <thead>
+ <tr>
+ <th><strong>Description</strong></th>
+ <th><strong>Taxes</strong></th>
+ <th class="text-center"><strong>Date Req.</strong></th>
+ <th class="text-right"><strong>Qty</strong></th>
+ <th class="text-right"><strong>Unit Price</strong></th>
+ <th class="text-right"><strong>Net Price</strong></th>
+ </tr>
+ </thead>
+ <tbody>
+ <tr t-foreach="o.order_line" t-as="line">
+ <td>
+ <span t-field="line.name"/>
+ </td>
+ <td>
+ <span t-esc="', '.join(map(lambda x: x.name, line.taxes_id))"/>
+ </td>
+ <td class="text-center">
+ <span t-field="line.date_planned"/>
+ </td>
+ <td class="text-right">
+ <span t-field="line.product_qty"/>
+ <span t-field="line.product_uom.name" groups="product.group_uom"/>
+ </td>
+ <td class="text-right">
+ <span t-field="line.price_unit"/>
+ </td>
+ <td class="text-right">
+ <span t-field="line.price_subtotal"
+ t-field-options='{"widget": "monetary", "display_currency": "o.pricelist_id.currency_id"}'/>
+ </td>
+ </tr>
+ </tbody>
+ </table>
+
+ <div class="row">
+ <div class="col-xs-4 pull-right">
<table class="table table-condensed">
- <thead>
- <tr>
- <th><strong>Description</strong></th>
- <th><strong>Taxes</strong></th>
- <th class="text-center"><strong>Date Req.</strong></th>
- <th class="text-right"><strong>Qty</strong></th>
- <th class="text-right"><strong>Unit Price</strong></th>
- <th class="text-right"><strong>Net Price</strong></th>
- </tr>
- </thead>
- <tbody>
- <tr t-foreach="o.order_line" t-as="line">
- <td>
- <span t-field="line.name"/>
- </td>
- <td>
- <span t-esc="', '.join(map(lambda x: x.name, line.taxes_id))"/>
- </td>
- <td class="text-center">
- <span t-field="line.date_planned"/>
- </td>
- <td class="text-right">
- <span t-field="line.product_qty"/>
- <span t-field="line.product_uom.name" groups="product.group_uom"/>
- </td>
- <td class="text-right">
- <span t-field="line.price_unit"/>
- </td>
- <td class="text-right">
- <span t-field="line.price_subtotal"
- t-field-options='{"widget": "monetary", "display_currency": "o.pricelist_id.currency_id"}'/>
- </td>
- </tr>
- </tbody>
+ <tr class="border-black">
+ <td><strong>Total Without Taxes</strong></td>
+ <td class="text-right">
+ <span t-field="o.amount_untaxed"
+ t-field-options='{"widget": "monetary", "display_currency": "o.pricelist_id.currency_id"}'/>
+ </td>
+ </tr>
+ <tr>
+ <td>Taxes</td>
+ <td class="text-right">
+ <span t-field="o.amount_tax"
+ t-field-options='{"widget": "monetary", "display_currency": "o.pricelist_id.currency_id"}'/>
+ </td>
+ </tr>
+ <tr class="border-black">
+ <td><strong>Total</strong></td>
+ <td class="text-right">
+ <span t-field="o.amount_total"
+ t-field-options='{"widget": "monetary", "display_currency": "o.pricelist_id.currency_id"}'/>
+ </td>
+ </tr>
</table>
-
- <div class="row">
- <div class="col-xs-4 pull-right">
- <table class="table table-condensed">
- <tr class="border-black">
- <td><strong>Total Without Taxes</strong></td>
- <td class="text-right">
- <span t-field="o.amount_untaxed"
- t-field-options='{"widget": "monetary", "display_currency": "o.pricelist_id.currency_id"}'/>
- </td>
- </tr>
- <tr>
- <td>Taxes</td>
- <td class="text-right">
- <span t-field="o.amount_tax"
- t-field-options='{"widget": "monetary", "display_currency": "o.pricelist_id.currency_id"}'/>
- </td>
- </tr>
- <tr class="border-black">
- <td><strong>Total</strong></td>
- <td class="text-right">
- <span t-field="o.amount_total"
- t-field-options='{"widget": "monetary", "display_currency": "o.pricelist_id.currency_id"}'/>
- </td>
- </tr>
- </table>
- </div>
- </div>
-
- <p t-field="o.notes"/>
- <div class="oe_structure"/>
</div>
- </t>
+ </div>
+
+ <p t-field="o.notes"/>
+ <div class="oe_structure"/>
+ </div>
+ </t>
+</template>
+
+<template id="report_purchaseorder">
+ <t t-call="report.html_container">
+ <t t-foreach="doc_ids" t-as="doc_id">
+ <t t-raw="translate_doc(doc_id, doc_model, 'partner_id.lang', 'purchase.report_purchaseorder_document')"/>
</t>
</t>
</template>
<?xml version="1.0" encoding="utf-8"?>
<openerp>
<data>
-<template id="report_purchasequotation">
- <t t-call="report.html_container">
- <t t-foreach="docs" t-as="o">
- <t t-call="report.external_layout">
- <div class="page">
- <div class="oe_structure"/>
+<template id="report_purchasequotation_document">
+ <t t-call="report.external_layout">
+ <div class="page">
+ <div class="oe_structure"/>
- <div class="row mt32 mb32">
- <div class="col-xs-6">
- Shipping address :<br/>
- <div t-if="o.dest_address_id">
- <div t-field="o.dest_address_id"
- t-field-options='{"widget": "contact", "fields": ["address", "name", "phone", "fax"], "no_marker": true}'/>
- <p t-if="o.partner_id.vat">VAT: <span t-field="o.partner_id.vat"/></p>
- </div>
- <div t-if="o.picking_type_id.warehouse_id">
- <span t-field="o.picking_type_id.warehouse_id.name"/>
- <div t-field="o.picking_type_id.warehouse_id.partner_id"
- t-field-options='{"widget": "contact", "fields": ["address", "name", "phone", "fax"], "no_marker": true}'/>
- <p t-if="o.partner_id.vat">VAT: <span t-field="o.partner_id.vat"/></p>
- </div>
- </div>
- <div class="col-xs-5 col-xs-offset-1">
- <div t-field="o.partner_id"
- t-field-options='{"widget": "contact", "fields": ["address", "name", "phone", "fax"], "no_marker": true}'/>
- </div>
+ <div class="row mt32 mb32">
+ <div class="col-xs-6">
+ Shipping address :<br/>
+ <div t-if="o.dest_address_id">
+ <div t-field="o.dest_address_id"
+ t-field-options='{"widget": "contact", "fields": ["address", "name", "phone", "fax"], "no_marker": true}'/>
+ <p t-if="o.partner_id.vat">VAT: <span t-field="o.partner_id.vat"/></p>
+ </div>
+ <div t-if="o.picking_type_id.warehouse_id">
+ <span t-field="o.picking_type_id.warehouse_id.name"/>
+ <div t-field="o.picking_type_id.warehouse_id.partner_id"
+ t-field-options='{"widget": "contact", "fields": ["address", "name", "phone", "fax"], "no_marker": true}'/>
+ <p t-if="o.partner_id.vat">VAT: <span t-field="o.partner_id.vat"/></p>
</div>
+ </div>
+ <div class="col-xs-5 col-xs-offset-1">
+ <div t-field="o.partner_id"
+ t-field-options='{"widget": "contact", "fields": ["address", "name", "phone", "fax"], "no_marker": true}'/>
+ </div>
+ </div>
- <h2>Request for Quotation <span t-field="o.name"/></h2>
+ <h2>Request for Quotation <span t-field="o.name"/></h2>
- <table class="table table-condensed">
- <thead>
- <tr>
- <th><strong>Description</strong></th>
- <th class="text-center"><strong>Expected Date</strong></th>
- <th class="text-right"><strong>Qty</strong></th>
- </tr>
- </thead>
- <tbody>
- <tr t-foreach="o.order_line" t-as="order_line">
- <td>
- <span t-field="order_line.name"/>
- </td>
- <td class="text-center">
- <span t-field="order_line.date_planned"/>
- </td>
- <td class="text-right">
- <span t-field="order_line.product_qty"/>
- <span t-field="order_line.product_uom" groups="product.group_uom"/>
- </td>
- </tr>
- </tbody>
- </table>
+ <table class="table table-condensed">
+ <thead>
+ <tr>
+ <th><strong>Description</strong></th>
+ <th class="text-center"><strong>Expected Date</strong></th>
+ <th class="text-right"><strong>Qty</strong></th>
+ </tr>
+ </thead>
+ <tbody>
+ <tr t-foreach="o.order_line" t-as="order_line">
+ <td>
+ <span t-field="order_line.name"/>
+ </td>
+ <td class="text-center">
+ <span t-field="order_line.date_planned"/>
+ </td>
+ <td class="text-right">
+ <span t-field="order_line.product_qty"/>
+ <span t-field="order_line.product_uom" groups="product.group_uom"/>
+ </td>
+ </tr>
+ </tbody>
+ </table>
- <p t-field="o.notes"/>
- <span>Regards,</span>
- <span t-field="user.signature"/>
+ <p t-field="o.notes"/>
+ <span>Regards,</span>
+ <span t-field="user.signature"/>
- <div class="oe_structure"/>
- </div>
- </t>
+ <div class="oe_structure"/>
+ </div>
+ </t>
+</template>
+
+<template id="report_purchasequotation">
+ <t t-call="report.html_container">
+ <t t-foreach="doc_ids" t-as="doc_id">
+ <t t-raw="translate_doc(doc_id, doc_model, 'partner_id.lang', 'purchase.report_purchasequotation_document')"/>
</t>
</t>
</template>
var c = openerp.webclient.crashmanager;
if (action.report_type == 'qweb-html') {
- window.open(report_url, '_blank', 'height=900,width=1280');
+ window.open(report_url, '_blank', 'scrollbars=1,height=900,width=1280');
instance.web.unblockUI();
} else if (action.report_type === 'qweb-pdf') {
// Trigger the download of the pdf/controller report
flag = False
break
if flag:
- workflow.trg_validate(uid, 'sale.order', order.id, 'manual_invoice', cr)
+ line.order_id.write({'state': 'progress'})
+ workflow.trg_validate(uid, 'sale.order', order.id, 'all_lines', cr)
if not invoices:
raise osv.except_osv(_('Warning!'), _('Invoice cannot be created for this Sales Order Line due to one of the following reasons:\n1.The state of this sales order line is either "draft" or "cancel"!\n2.The Sales Order Line is Invoiced!'))
frm_cur = self.pool.get('res.users').browse(cr, uid, uid).company_id.currency_id.id
to_cur = self.pool.get('product.pricelist').browse(cr, uid, [pricelist])[0].currency_id.id
if product:
- purchase_price = self.pool.get('product.product').browse(cr, uid, product).standard_price
+ product = self.pool['product.product'].browse(cr, uid, product, context=context)
+ purchase_price = product.standard_price
+ to_uom = res.get('product_uom', uom)
+ if to_uom != product.uom_id.id:
+ purchase_price = self.pool['product.uom']._compute_price(cr, uid, product.uom_id.id, purchase_price, to_uom)
ctx = context.copy()
ctx['date'] = date_order
price = self.pool.get('res.currency').compute(cr, uid, frm_cur, to_cur, purchase_price, round=False, context=ctx)
<t t-foreach="values" t-as="id">
<t t-set="file" t-value="widget.data[id]"/>
<div class="oe_attachment">
- <span t-if="(file.upload or file.percent_loaded<100)" t-attf-title="{(file.name || file.filename) + (file.date?' \n('+file.date+')':'' )}" t-attf-name="{file.name || file.filename}">
+ <span t-if="(file.upload or file.percent_loaded<100)" t-attf-title="#{(file.name || file.filename) + (file.date?' \n('+file.date+')':'' )}" t-attf-name="#{file.name || file.filename}">
<span class="oe_fileuploader_in_process">...Upload in progress...</span>
<t t-raw="file.name || file.filename"/>
</span>
- <a t-if="(!file.upload or file.percent_loaded>=100)" t-att-href="file.url" t-attf-title="{(file.name || file.filename) + (file.date?' \n('+file.date+')':'' )}">
+ <a t-if="(!file.upload or file.percent_loaded>=100)" t-att-href="file.url" t-attf-title="#{(file.name || file.filename) + (file.date?' \n('+file.date+')':'' )}">
<t t-raw="file.name || file.filename"/>
</a>
<t t-if="(!file.upload or file.percent_loaded>=100)">
- <a class="oe_right oe_delete oe_e" title="Delete this file" t-attf-data-id="{file.id}">[</a>
+ <a class="oe_right oe_delete oe_e" title="Delete this file" t-attf-data-id="#{file.id}">[</a>
</t>
</div>
</t>
<t t-foreach="widget.get('value')" t-as="id">
<t t-set="file" t-value="widget.data[id]"/>
<div>
- <a t-att-href="file.url" t-attf-title="{(file.name || file.filename) + (file.date?' \n('+file.date+')':'' )}">
+ <a t-att-href="file.url" t-attf-title="#{(file.name || file.filename) + (file.date?' \n('+file.date+')':'' )}">
<t t-raw="file.name || file.filename"/>
</a>
</div>
}
else {
var pop = new instance.web.form.FormOpenPopup(this);
- pop.show_element(this.dataset.model, id, this.dataset.get_context(), {
+ pop.show_element(this.dataset.model, parseInt(id), this.dataset.get_context(), {
title: _.str.sprintf(_t("View: %s"),title),
view_id: +this.open_popup_action,
res_id: id,
for tr in transitions:
list_tr.append(tr)
connectors.setdefault(tr, {
- 'id': tr,
+ 'id': int(tr),
's_id': transitions[tr][0],
'd_id': transitions[tr][1]
})
Date/datetime
-------------
-Dates and datetimes are always a little tricky. There is a special syntax for grouping them by intervals.
+Dates and datetimes are always a little tricky. There is a special syntax for grouping them by intervals. Most of the time, the interval can be specified as a suffix:
* field_date:day,
* field_date:week,
.. code-block:: xml
+ <filter string="Week" context="{'group_by':'date_followup:week'}" help="Week"/>
+
+But to describe a graph view in xml, this would fail the xml validation ("date_followup:week" is not a valid field). In that case, the graph view can be described with an "interval" attribute. For example,
+
+.. code-block:: xml
+
<graph string="Leads Analysis" type="pivot" stacked="True">
- <field name="date_deadline:week" type="row"/>
+ <field name="date_deadline" interval="week" type="row"/>
<field name="stage_id" type="col"/>
<field name="planned_revenue" type="measure"/>
</graph>
-
Example:
--------
Here is an example of a graph view defined for the model *crm.lead.report*. It will open in pivot table mode. If it is switched to bar chart mode, the bars will be stacked. The data will be grouped according to the date_deadline field in rows, and the columns will be the various stages of an opportunity. Also, the *planned_revenue* field will be used as a measure.
t-att-data-oe-company-name="res_company.name">
<head>
<meta charset="utf-8" />
- <t t-if="main_object and 'website_meta_title' in main_object">
+ <t t-if="main_object and 'website_meta_title' in main_object and not title">
<t t-set="title" t-value="main_object.website_meta_title"/>
</t>
- <t t-if="not title and main_object and 'name' in main_object">
+ <t t-if="main_object and 'name' in main_object and not title and not additional_title">
<t t-set="additional_title" t-value="main_object.name"/>
</t>
<t t-if="not title">
- <t t-set="title"><t t-raw="res_company.name"/><t t-if="additional_title"> - <t t-raw="additional_title"/></t></t>
+ <t t-set="title"><t t-if="additional_title"><t t-raw="additional_title"/> | </t><t t-esc="(website or res_company).name"/></t>
</t>
+
<meta name="viewport" content="initial-scale=1"/>
<meta name="description" t-att-content="main_object and 'website_meta_description' in main_object
and main_object.website_meta_description or website_meta_description"/>
'res.users', 'Last Contributor',
select=True, readonly=True,
),
+ 'author_avatar': fields.related(
+ 'author_id', 'image_small',
+ string="Avatar", type="binary"),
'visits': fields.integer('No of Views'),
'ranking': fields.function(_compute_ranking, string='Ranking', type='float'),
}
</div>
<div t-foreach="blog_posts" t-as="blog_post" class="mb32">
-
- <img class="img-circle pull-right mt16"
- t-att-src="website.image_url(blog_post.author_id, 'image_small')"
- style="width: 50px;"/>
-
+ <span t-field="blog_post.author_avatar" t-field-options='{"widget": "image", "class": "img-circle pull-right mt16 media-object"}' />
<a t-attf-href="/blog/#{ slug(blog_post.blog_id) }/post/#{ slug(blog_post) }">
<h2 t-field="blog_post.name" class="mb4"/>
</a>
if category:
url = "/shop/category/%s" % slug(category)
pager = request.website.pager(url=url, total=product_count, page=page, step=PPG, scope=7, url_args=post)
- product_ids = product_obj.search(cr, uid, domain, limit=PPG+10, offset=pager['offset'], order='website_published desc, website_sequence desc', context=context)
+ product_ids = product_obj.search(cr, uid, domain, limit=PPG, offset=pager['offset'], order='website_published desc, website_sequence desc', context=context)
products = product_obj.browse(cr, uid, product_ids, context=context)
style_obj = pool['product.style']
shipping_ids = []
checkout = {}
if not data:
- print request.uid, request.website.user_id.id
if request.uid != request.website.user_id.id:
checkout.update( self.checkout_parse("billing", partner) )
shipping_ids = orm_partner.search(cr, SUPERUSER_ID, [("parent_id", "=", partner.id), ('type', "=", 'delivery')], context=context)
else:
query = dict((prefix + field_name, getattr(data, field_name))
for field_name in all_fields if getattr(data, field_name))
- if data.parent_id:
+ if address_type == 'billing' and data.parent_id:
query[prefix + 'street'] = data.parent_id.name
if query.get(prefix + 'state_id'):
var value = +$shippingDifferent.val();
var data = $shippingDifferent.find("option:selected").data();
var $snipping = $(".js_shipping", oe_website_sale);
- var $inputs = $snipping.find("input,select");
+ var $inputs = $snipping.find("input");
+ var $selects = $snipping.find("select");
$snipping.toggle(!!value);
$inputs.attr("readonly", value <= 0 ? null : "readonly" ).prop("readonly", value <= 0 ? null : "readonly" );
+ $selects.attr("disabled", value <= 0 ? null : "disabled" ).prop("disabled", value <= 0 ? null : "disabled" );
$inputs.each(function () {
$(this).val( data[$(this).attr("name")] || "" );
t-att-data-shipping_country_id="shipping.country_id and shipping.country_id.id"
><t t-esc="', '.join('\n'.join(shipping.name_get()[0][1].split(',')).split('\n')[1:])"/></option>
</t>
- <option value="-1">-- Create a new address --</option>
+ <option value="-1" t-att-selected="error and len(error) > 0 and shipping_id == -1">-- Create a new address --</option>
</select>
</div>
</div>
</div>
<div t-attf-class="form-group #{error.get('shipping_country_id') and 'has-error' or ''} col-lg-6">
<label class="control-label" for="shipping_country_id">Country</label>
- <select name="shipping_country_id" class="form-control" t-att-readonly=" 'readonly' if shipping_id >= 0 else ''">
+ <select name="shipping_country_id" class="form-control" t-att-disabled=" 'disabled' if shipping_id >= 0 else ''">
<option value="">Country...</option>
<t t-foreach="countries or []" t-as="country">
<option t-att-value="country.id" t-att-selected="country.id == checkout.get('shipping_country_id')"><t t-esc="country.name"/></option>
case "${1}" in
configure)
if ! getent passwd | grep -q "^odoo:"; then
- adduser --system --no-create-home --quiet --group $ODOO_USER
+ adduser --system --home $ODOO_DATA_DIR --quiet --group $ODOO_USER
fi
# Register "$ODOO_USER" as a postgres superuser
su - postgres -c "createuser -s $ODOO_USER" 2> /dev/null || true
chown $ODOO_USER:$ODOO_GROUP $ODOO_LOG_DIR
chmod 0750 $ODOO_LOG_DIR
# Data dir
- mkdir -p $ODOO_DATA_DIR
chown $ODOO_USER:$ODOO_GROUP $ODOO_DATA_DIR
# update-python-modules NOW otherwise invoke-rc.d openerp start will fail
update-python-modules
import importlib
import os.path
from urlparse import urlunsplit
-import sphinx
def setup(app):
app.add_config_value('github_user', None, 'env')
if not app.config.github_user and app.config.github_project:
return
- def github_doc_link(mode='blob'):
- """ returns the github URL for the current page
-
- :param str mode: 'edit' for edition view
- """
- return make_github_link(
- app,
- 'doc/%s%s' % (pagename, app.config.source_suffix),
- mode=mode)
- context['github_link'] = github_doc_link
+ # can't use functools.partial because 3rd positional is line not mode
+ context['github_link'] = lambda mode='mode': make_github_link(
+ app, 'doc/%s%s' % (pagename, app.config.source_suffix), mode=mode)
{{ toctree(maxdepth=4, collapse=False, includehidden=True,
main_navbar=False, titles_only=False) }}
{% if github_link %}
- <p><a href="{{ github_link() }}" class="github">
- View on GitHub
+ <p><a href="{{ github_link(mode='edit') }}" class="github">
+ Edit on GitHub
</a></p>
{% endif %}
</div>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
- <a href="http://odoo.com" class="o_logo navbar-brand">
+ <a href="{{ pathto(master_doc) }}" class="o_logo navbar-brand">
<span class="o_logo_main">odoo</span><span class="o_logo_app">doc</span>
</a>
{% if versions %}
{%- if show_copyright %}
<li>
{%- if hasdoc('copyright') %}
- {% trans path=pathto('copyright'), copyright=copyright|e %}© <a href="{{ path }}">Copyright</a> {{ copyright }}.{% endtrans %}
+ {% trans path=pathto('copyright'), copyright=copyright|e %}© <a href="{{ path }}">Copyright</a> <a href="https://odoo.com">{{ copyright }}</a>.{% endtrans %}
{%- else %}
- {% trans copyright=copyright|e %}© Copyright {{ copyright }}.{% endtrans %}
+ {% trans copyright=copyright|e %}© Copyright <a href="https://odoo.com">{{ copyright }}</a>.{% endtrans %}
{%- endif %}
</li>
{%- endif %}
</p>
</div>
</div>
+ {%- if google_analytics_key -%}
+ <script>
+ (function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
+ (i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
+ m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
+ })(window,document,'script','//www.google-analytics.com/analytics.js','ga');
+
+ ga('create', '{{ google_analytics_key }}', 'auto');
+ ga('send','pageview');
+ </script>
+ {%- endif -%}
{%- endblock %}
# General information about the project.
project = u'odoo'
-copyright = u'2014, OpenERP s.a.'
+copyright = u'OpenERP S.A.'
# The version info for the project you're documenting, acts as replacement for
# |version| and |release|, also used in various other places throughout the
# The short X.Y version.
version = '8.0'
# The full version, including alpha/beta/rc tags.
-release = '8.0b1'
+release = '8.0'
# There are two options for replacing |today|: either, you set today to some
# non-false value, then it is used:
app.connect('html-page-context', versionize)
app.add_config_value('versions', '', 'env')
+ app.connect('html-page-context', analytics)
+ app.add_config_value('google_analytics_key', False, 'env')
+
def canonicalize(app, pagename, templatename, context, doctree):
""" Adds a 'canonical' URL for the current document in the rendering
context. Requires the ``canonical_root`` setting being set. The canonical
if vs != app.config.version
]
+def analytics(app, pagename, templatename, context, doctree):
+ if not app.config.google_analytics_key:
+ return
+
+ context['google_analytics_key'] = app.config.google_analytics_key
+
def _build_url(root, branch, pagename):
return "{canonical_url}{canonical_branch}/{canonical_page}".format(
canonical_url=root,
+++ /dev/null
-======
-Guides
-======
-
-.. toctree::
- :titlesonly:
-
- guides/forms
- guides/themes
- guides/snippets
- guides/workflows
- guides/deployment
+++ /dev/null
-.. _guides/deployment:
-
-=======================
-Deploying to production
-=======================
+++ /dev/null
-.. highlight:: xml
-
-.. _form-view-guidelines:
-
-Form Views Guidelines
-=====================
-
-.. sectionauthor:: Aline Preillon, Raphael Collet
-
-This document presents functional and technical guidelines for
-creating/organizing form views in Odoo. For each item, both the functional and
-technical aspects are explained. The goal of the new style of forms is to make
-Odoo easier to use, and to guide users through the system.
-
-Business Views
---------------
-
-Business views are targeted at regular users, not advanced users. Examples
-are: Opportunities, Products, Partners, Tasks, Projects, etc.
-
-.. image:: forms/oppreadonly.png
- :class: img-responsive
-
-In general, a business view is composed of
-
-1. a status bar on top (with technical or business flow),
-2. a sheet in the middle (the form itself),
-3. a bottom part with History and Comments.
-
-Technically, the new form views are structured as follows in XML::
-
- <form>
- <header> ... content of the status bar ... </header>
- <sheet> ... content of the sheet ... </sheet>
- <div class="oe_chatter"> ... content of the bottom part ... </div>
- </form>
-
-The Status Bar
-''''''''''''''
-
-The purpose of the status bar is to show the status of the current record and
-the action buttons.
-
-.. image:: forms/status.png
- :class: img-responsive
-
-The Buttons
-...........
-
-The order of buttons follows the business flow. For instance, in a sale order,
-the logical steps are:
-
-1. Send the quotation
-2. Confirm the quotation
-3. Create the final invoice
-4. Send the goods
-
-Highlighted buttons (in red by default) emphasize the logical next step, to
-help the user. It is usually the first active button. On the other hand,
-:guilabel:`cancel` buttons *must* remain grey (normal). For instance, in
-Invoice the button :guilabel:`Refund` must never be red.
-
-Technically, buttons are highlighted by adding the class "oe_highlight"::
-
- <button class="oe_highlight" name="..." type="..." states="..."/>
-
-The Status
-..........
-
-Uses the ``statusbar`` widget, and shows the current state in red. States
-common to all flows (for instance, a sale order begins as a quotation, then we
-send it, then it becomes a full sale order, and finally it is done) should be
-visible at all times but exceptions or states depending on particular sub-flow
-should only be visible when current.
-
-.. image:: forms/status1.png
- :class: img-responsive
-
-.. image:: forms/status2.png
- :class: img-responsive
-
-The states are shown following the order used in the field (the list in a
-selection field, etc). States that are always visible are specified with the
-attribute ``statusbar_visible``.
-
-``statusbar_colors`` can be used to give a custom color to specific states.
-
-::
-
- <field name="state" widget="statusbar"
- statusbar_visible="draft,sent,progress,invoiced,done"
- statusbar_colors="{'shipping_except':'red','waiting_date':'blue'}"/>
-
-The Sheet
-'''''''''
-
-All business views should look like a printed sheet:
-
-.. image:: forms/sheet.png
- :class: img-responsive
-
-1. Elements inside a ``<form>`` or ``<page>`` do not define groups, elements
- inside them are laid out according to normal HTML rules. They content can
- be explicitly grouped using ``<group>`` or regular ``<div>`` elements.
-2. By default, the element ``<group>`` defines two columns inside, unless an
- attribute ``col="n"`` is used. The columns have the same width (1/n th of
- the group's width). Use a ``<group>`` element to produce a column of fields.
-3. To give a title to a section, add a ``string`` attribute to a ``<group>`` element::
-
- <group string="Time-sensitive operations">
-
- this replaces the former use of ``<separator string="XXX"/>``.
-4. The ``<field>`` element does not produce a label, except as direct children
- of a ``<group>`` element\ [#backwards-compatibility]_. Use :samp:`<label
- for="{field_name}>` to produce a label of a field.
-
-Sheet Headers
-.............
-
-Some sheets have headers with one or more fields, and the labels of those
-fields are only shown in edit mode.
-
-.. list-table::
- :header-rows: 1
-
- * - View mode
- - Edit mode
- * - .. image:: forms/header.png
- :class: img-responsive
- - .. image:: forms/header2.png
- :class: img-responsive
-
-Use HTML text, ``<div>``, ``<h1>``, ``<h2>``… to produce nice headers, and
-``<label>`` with the class ``oe_edit_only`` to only display the field's label
-in edit mode. The class ``oe_inline`` will make fields inline (instead of
-blocks): content following the field will be displayed on the same line rather
-than on the line below it. The form above is produced by the following XML::
-
- <label for="name" class="oe_edit_only"/>
- <h1><field name="name"/></h1>
-
- <label for="planned_revenue" class="oe_edit_only"/>
- <h2>
- <field name="planned_revenue" class="oe_inline"/>
- <field name="company_currency" class="oe_inline oe_edit_only"/> at
- <field name="probability" class="oe_inline"/> % success rate
- </h2>
-
-Button Box
-..........
-
-Many relevant actions or links can be displayed in the form. For example, in
-Opportunity form, the actions "Schedule a Call" and "Schedule a Meeting" take
-an important place in the use of the CRM. Instead of placing them in the
-"More" menu, put them directly in the sheet as buttons (on the top right) to
-make them more visible and more easily accessible.
-
-.. image:: forms/header3.png
- :class: img-responsive
-
-Technically, the buttons are placed inside a <div> to group them as a block on
-the right-hand side of the sheet.
-
-::
-
- <div class="oe_button_box oe_right">
- <button string="Schedule/Log Call" name="..." type="action"/>
- <button string="Schedule Meeting" name="action_makeMeeting" type="object"/>
- </div>
-
-Groups and Titles
-.................
-
-A column of fields is now produced with a ``<group>`` element, with an
-optional title.
-
-.. image:: forms/screenshot-03.png
- :class: img-responsive
-
-::
-
- <group string="Payment Options">
- <field name="writeoff_amount"/>
- <field name="payment_option"/>
- </group>
-
-It is recommended to have two columns of fields on the form. For this, simply
-put the ``<group>`` elements that contain the fields inside a top-level
-``<group>`` element.
-
-To make :ref:`view extension <reference/views/inheritance>` simpler, it is
-recommended to put a ``name`` attribute on ``<group>`` elements, so new fields
-can easily be added at the right place.
-
-Special Case: Subtotals
-~~~~~~~~~~~~~~~~~~~~~~~
-
-Some classes are defined to render subtotals like in invoice forms:
-
-.. image:: forms/screenshot-00.png
- :class: img-responsive
-
-::
-
- <group class="oe_subtotal_footer">
- <field name="amount_untaxed"/>
- <field name="amount_tax"/>
- <field name="amount_total" class="oe_subtotal_footer_separator"/>
- <field name="residual" style="margin-top: 10px"/>
- </group>
-
-Placeholders and Inline Fields
-..............................
-
-Sometimes field labels make the form too complex. One can omit field labels,
-and instead put a placeholder inside the field. The placeholder text is
-visible only when the field is empty. The placeholder should tell what to
-place inside the field, it *must not* be an example as they are often confused
-with filled data.
-
-One can also group fields together by rendering them "inline" inside an
-explicit block element like `<div>``. This allows grouping semantically
-related fields as if they were a single (composite) fields.
-
-The following example, taken from the *Leads* form, shows both placeholders and
-inline fields (zip and city).
-
-.. list-table::
- :header-rows: 1
-
- * - Edit mode
- - View mode
- * - .. image:: forms/placeholder.png
- :class: img-responsive
- - .. image:: forms/screenshot-01.png
- :class: img-responsive
-
-::
-
- <group>
- <label for="street" string="Address"/>
- <div>
- <field name="street" placeholder="Street..."/>
- <field name="street2"/>
- <div>
- <field name="zip" class="oe_inline" placeholder="ZIP"/>
- <field name="city" class="oe_inline" placeholder="City"/>
- </div>
- <field name="state_id" placeholder="State"/>
- <field name="country_id" placeholder="Country"/>
- </div>
- </group>
-
-Images
-......
-
-Images, like avatars, should be displayed on the right of the sheet. The
-product form looks like:
-
-.. image:: forms/screenshot-02.png
- :class: img-responsive
-
-The form above contains a <sheet> element that starts with::
-
- <field name="product_image" widget="image" class="oe_avatar oe_right"/>
-
-Tags
-....
-
-Most :class:`~openerp.fields.Many2many` fields, like categories, are better
-rendered as a list of tags. Use the widget ``many2many_tags`` for this:
-
-.. image:: forms/screenshot-04.png
- :class: img-responsive
-
-::
-
- <field name="category_id"
- widget="many2many_tags"/>
-
-Task-based forms
-----------------
-
-Configuration Forms
-'''''''''''''''''''
-
-Examples of configuration forms: Stages, Leave Type, etc. This concerns all
-menu items under Configuration of each application (like Sales/Configuration).
-
-.. image:: forms/nosheet.png
- :class: img-responsive
-
-For those views, the guidelines are:
-
-1. no header (because no state, no workflow, no button)
-2. no sheet
-
-Regular Wizards (dialog)
-''''''''''''''''''''''''
-
-Example: "Schedule a Call" from an opportunity.
-
-.. image:: forms/wizard-popup.png
- :class: img-responsive
-
-The guidelines are:
-
-1. avoid separators (the title is already in the popup title bar, so another
- separator is not relevant)
-2. avoid cancel buttons (user generally close the popup window to get the same
- effect)
-3. action buttons must be highlighted (red)
-4. when there is a text area, use a placeholder instead of a label or a
- separator
-5. like in regular form views, put buttons in the <header> element
-
-Configuration Wizard
-''''''''''''''''''''
-
-Example: Settings / Configuration / Sales. The guidelines are:
-
-1. always in line (no popup)
-2. no sheet
-3. keep the cancel button (users cannot close the window)
-4. the button "Apply" must be red
-
-.. [#backwards-compatibility] for backwards compatibility reasons
+++ /dev/null
-========
-Snippets
-========
+++ /dev/null
-.. highlight:: xml
-
-===============
-Creating themes
-===============
-
-Basic set up
-============
-
-Create a basic theme module with :command:`odoo.py scaffold` and the ``theme``
-template: from the root odoo folder, use
-
-.. code-block:: console
-
- $ ./odoo.py scaffold -t theme "Dummy Theme" addons
-
-this should create a new folder ``dummy_theme`` in the ``addons`` directory
-with the structure:
-
-.. code-block:: text
-
- addons/dummy_theme
- |-- __init__.py
- |-- __openerp__.py
- |-- static
- | `-- style
- | `-- custom.less
- `-- views
- |-- options.xml
- |-- pages.xml
- `-- snippets.xml
-
-``static/styles`` contains your stylesheet(s), ``views`` contains the various
-XML files describing the theme and theme features to Odoo.
-
-Static Page
------------
-
-Creating a new template
-'''''''''''''''''''''''
-
-Create a new file :file:`odoo/addons/theme_dummy/views/pages.xml` and open it.
-
-In odoo, a page means a new template. You don't need special skills, simply
-copy paste the lines::
-
- <template id="website.hello" name="Homepage" page="True">
- <t t-call="website.layout">
- <div id="wrap" class="oe_structure oe_empty">
- </div>
- </t>
- </template>
-
-Refresh the page and feel the hit.
-
-Editing content on a page
-'''''''''''''''''''''''''
-
-You can now add you content! You should always use the Bootstrap structure as
-below::
-
- <template id="website.hello" name="Homepage" page="True">
- <t t-call="website.layout">
- <div id="wrap" class="oe_structure oe_empty">
- <section>
- <div class="container">
- <div class="row">
- <h1>This is Your Content</h1>
- <p>Isn't amazing to edit everything inline?</p>
- <hr/>
- </div>
- </div>
- </section>
- </div>
- </t>
- </template>
-
-Adding new item in the menu
-'''''''''''''''''''''''''''
-
-Adding these few more lines will put the new page in your menu::
-
- <record id="hello_menu" model="website.menu">
- <field name="name">Hello</field>
- <field name="url">/page/hello</field>
- <field name="parent_id" ref="website.main_menu"/>
- <field name="sequence" type="int">20</field>
- </record>
-
-Congrats! It's online! Now drag and drop some snippets on the page and let's
-style!
-
-Pimp Your Theme
----------------
-
-Easy styling with less
-''''''''''''''''''''''
-
-In ``odoo/addons/theme_dummy/static`` create a new folder and name it
-``style``. In the new folder ``odoo/addons/theme_dummy/static/style`` create a
-file and name it ``custom.less``. Open ``custom.less`` in the text editor and
-modify these lines as below:
-
-
-.. code-block:: css
-
- .h1 {
- color: #215487;
- }
- .span {
- border: 2px solid black;
- background-color: #eee;
- }
-
-Refresh the page and feel the hit.
-
-Get the most of the dom
-'''''''''''''''''''''''
-
-Right-Click, inspect element. You can go deeper by styling the main layout
-container. Here we try with the 'wrapwrap' id.
-
-.. code-block:: css
-
- #wrapwrap {
- background-color: #222;
- width: 80%;
- margin: 0 auto;
- }
-
-Easy layout with bootstrap
-''''''''''''''''''''''''''
-
-Open :file:`odoo/addons/theme_dummy/views/pages.xml` and add a new section::
-
- <section>
- <div class="container">
- <div class="row">
- <div class="alert alert-primary" role="alert">
- <a href="#" class="alert-link">...</a>
- </div>
- <div class="col-md-6 bg-blue">
- <h2>BLUE it!</h2>
- </div>
- <div class="col-md-6 bg-green">
- <h2>GREEN THAT!</h2>
- </div>
- </div>
- </div>
- </section>
-
-Refresh the page and check how it looks.
-
-The background of the alert component is the default Bootstrap primary color.
-The two other div your created have no custom styles applied yet. Open
-:file:`odoo/addons/theme_dummy/static/style/custom.less` and add these lines:
-
-.. code-block:: css
-
- @brand-primary: #1abc9c;
- @color-blue: #3498db;
- @color-green: #2ecc71;
-
- .bg-blue { background: @color-blue; }
- .bg-green { background: @color-green; }
-
- .h2 { color: white; }
-
-As you see, the default primary has changed and your new colors are shining!
-
-Build Your First Snippet
-------------------------
-
-Setting up __openerp__.py
-'''''''''''''''''''''''''
-
-Open ``__openerp__.py`` and add a new line as below:
-
-.. code-block:: python
-
- {
- 'name': 'Dummy Theme',
- 'description': 'Dummy Theme',
- 'category': 'Website',
- 'version': '1.0',
- 'author': 'OpenERP SA',
- 'depends': ['website'],
- 'data': [
- 'views/snippets.xml',
- ],
- 'application': True,
- }
-
-In ``odoo/addons/theme_learn/views`` create a new xml file, name it
-``snippets.xml`` and open it in a text editor
-
-Add your snippet in the menu
-''''''''''''''''''''''''''''
-
-Before typing your html code, you need to locate it in the WEBb. drop-down
-menu. In this case, we will add it at the end of the Structure section::
-
- <template id="snippets" inherit_id="website.snippets" name="Clean Theme snippets">
- <xpath expr="//div[@id='snippet_structure']" position="inside">
- </xpath>
- </template>
-
-Now open a new div, do not give it any id or classes. It will contain your
-snippet::
-
- <xpath expr="//div[@id='snippet_structure']" position="inside">
- <div>
- </div>
- </xpath>
-
-A thumbnail is also needed to create a more attractive link in the menu. You
-can use labels to focus on your themes snippets. Simply add a new div with
-the class ``oe_snippet_thumbnail`` and add your thumbnail image (100x79px)::
-
- <xpath expr="//div[@id='snippet_structure']" position="inside">
- <div>
- <div class="oe_snippet_thumbnail">
- <img class="oe_snippet_thumbnail_img" src="/theme_Dummy/static/img/blocks/block_title.png"/>
- <span class="oe_snippet_thumbnail_title">SNIP IT!</span>
- </div>
- </div>
- </xpath>
-
-And voila! Your new snippet is now ready to use. Just drag and drop it on your
-page to see it in action.
-
-The snippet body
-''''''''''''''''
-
-A snippet has to be in a section with the class ``oe_snippet_body`` to work
-correctly. As Odoo use the Bootstrap framework, you have use containers and
-rows to hold your content. Please refer the the Bootstrap documentation::
-
- <xpath expr="//div[@id='snippet_structure']" position="inside">
- <div>
- <div class="oe_snippet_thumbnail">
- <img class="oe_snippet_thumbnail_img" src="/theme_Dummy/static/img/blocks/block_title.png"/>
- <span class="oe_snippet_thumbnail_title">SNIP IT!</span>
- </div>
-
- <section class="oe_snippet_body fw_categories">
- <div class="container">
- <div class="row">
- </div>
- </div>
- </section>
- </div>
- </xpath>
-
-Inside your fresh new row, add some bootstraped contents::
-
- <div class="col-md-12 text-center mt32 mb32">
- <h2>A great Title</h2>
- <h3 class="text-muted ">And a great subtitle too</h3>
- <p>Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diam nonummy nibh euismod tincidunt ut laoreet dolore magna aliquam erat volutpat. </p>
- </div>
-
-
-Adding images to your snippet
-'''''''''''''''''''''''''''''
-
-You can easely add images in your snippets simply by setting up css
-backgrounds images.
-
-In ``odoo/addons/theme_dummy/static/`` create a new folder and name it
-``img``. Put your images there, in sub-folders if needed. Open
-:file:`odoo/addons/theme_dummy/static/style/custom.less`, add these lines
-
-.. code-block:: css
-
- @img-01: url("../img/img-boy.png");
- .dummy-boy { background-image: @img-01; }
-
- @img-02: url("../img/img-girl.png");
- .dummy-girl { background-image: @img-02; }
-
-In :file:`odoo/addons/theme_dummy/views/pages.xml` change the correspondant
-lines as below::
-
- <section>
- <div class="container">
- <div class="row dummy-bg">
- <div class="alert alert-primary" role="alert">
- <a href="#" class="alert-link">...</a>
- </div>
- <div class="col-md-6">
- <h2>BLUE it!</h2>
- <div class="dummy-boy">
- </div>
- </div>
- <div class="col-md-6">
- <h2>GREEN THAT!</h2>
- <div class="dummy-girl">
- </div>
- </div>
- </div>
- </div>
- </section>
-
-Your new snippet is now ready to use. Just drag and drop it on your page to
-see it in action.
-
-Advanced Customization
-======================
-
-Defining Your Theme Options
----------------------------
-
-Understanding XPath
-'''''''''''''''''''
-
-As your stylesheets are running on the whole website, giving more option to
-your snippets and applying them independently will push your design
-forward. In ``odoo/addons/theme_dummy/views/`` create a new file, name it
-``options.xml`` and add these lines::
-
- <template id="gourman_website_options_pattern" inherit_id="website.snippet_options">
- <xpath expr="//div[@data-option='dummy_options']//ul" position="after">
- </xpath>
- </template>
-
-Explain xpath
-"""""""""""""
-
-.. TODO:: syntax not correct (see website examples)
-
-Your option menu is now correctly set in the database, you can create an new dropdown menu.
-
-Let's say yout want three options which will change the text color and the background.
-In option.xml, add these lines inside the xpath::
-
- <li data-check_class="text-purple"><a>YOUR OPTION 1</a></li>
- <li class="dropdown-submenu">
- <a tabindex="-1" href="#">Your sub option</a>
- <ul class="dropdown-menu">
- <li data-select_class="bg-yellow"><a>YOUR OPTION 2</a></li>
- <li data-select_class="text-light-bg-dark"><a>YOUR OPTION 3</a></li>
- <li data-select_class=""><a>None</a></li>
- </ul>
- <li>
-
-Simple less css options
-'''''''''''''''''''''''
-
-In order to see these options in action, you have to write some new css
-classes. Open custom.css and add this new lines
-
-.. code-block:: css
-
- @color-purple: #2ecc71;
- @color-yellow: #2ecc71;
-
- .text-purple { color: @color-purple; }
- .bg-yellow { background-color: @color-yellow;}
- .text-light-bg-dark { color: #eee; background-color: #222;}
-
-Refresh the page. Select a snippet and click Customize. Choose one of your new
-options apply it.
-
-XPath & inherits
-''''''''''''''''
-
-You can also add images in your variables and use them on certain part of your
-pages, snippets or any html element.
-
-In :file:`odoo/addons/theme_dummy/static/style/custom.css` add these new lines
-
-.. code:: css
-
- @bg-01: url("../img/background/bg-blur.jpg");
-
- .bg-01 {
- background-image: @bg-01;
- }
-
-Now that you have set the background image, you can decide how and where the
-user can use it, for example, on a simple div.
-
-Open :file:`odoo/addons/theme_dummy/views/options.xml` and add this new xpath::
-
- <xpath expr="//div[@data-option='background-dummy']//ul" position="after">
- <ul class="dropdown-menu">
- <li data-value="bg-01">
- <a>Image 1</a>
- </li>
- </ul>
- </xpath>
-
-Your option is ready to be applied but you want it to be shown only a certain
-part of a snippet.
-
-Open :file:`odoo/addons/theme_dummy/views/snippets.xml` and add a new snippet
-with the method we learned previously::
-
- <xpath expr="//div[@id='snippet_structure']" position="inside">
- <div>
- <!-- Add a Thumbnail in the Website Builder drop-down menu -->
- <div class="oe_snippet_thumbnail">
- <img class="oe_snippet_thumbnail_img" src="/theme_Dummy/static/img/blocks/block_title.png"/>
- <span class="oe_snippet_thumbnail_title">Test OPTION</span>
- </div>
- <!-- Your Snippet content -->
- <section class="oe_snippet_body fw_categories">
- <div class="container">
- <div class="row">
- <div class="col-md-6 text-center mt32 mb32">
- <h2>NO OPTION</h2>
- <p>OFF</p>
- </div>
- <div class="col-md-6 text-center mt32 mb32 test-option">
- <h2>OPTION</h2>
- <p>This div has the 'test-option' class</p>
- </div>
- </div>
- </div>
- </section>
- </div>
- </xpath>
-
-As you see, the second ``col-md`` has a class named ``test-option``. We are
-going to specify where this option can be turned on by adding the
-``data-selector`` attribute.
-
-Go back to your ``options.xml`` files, add these new lines::
-
- <xpath expr="//div[@data-option='background-dummy']" position="attributes">
- <attribute name="data-selector">test-option</attribute>
- </xpath>
-
-Refresh your browser. You should now be able to add your image background on
-the left div only. The option is now available on each section but also on
-the left div with the custom class.
-
-The Image Database
-------------------
-
-Modifying the image database
-''''''''''''''''''''''''''''
-
-Odoo provides its own image library but you certainly want to adapt it to your
-design. Do not use the Media Manager uploading Tool to add image in your
-theme. The images url's will be lost on reload! Instead of uploading your
-images, you can create your own library and disable the old ones.
-
-In ``odoo/addons/theme_dummy/views/`` create a new file, name it
-``images.xml`` and add these lines::
-
- <record id="image_bg_blue" model="ir.attachment">
- <field name="name">bg_blue.jpg</field>
- <field name="datas_fname">bg_blue.jpg</field>
- <field name="res_model">ir.ui.view</field>
- <field name="type">url</field>
- <field name="url">/theme_clean/static/img/library/bg/bg_blue.jpg</field>
- </record>
-
-Your images is now available in your Media Manager. And your Theme has a
-total new look.
-
-Theme Selector
-==============
-
-Set Up
-------
-
-Understanding theme variants
-''''''''''''''''''''''''''''
-
-Combining theme variants
-''''''''''''''''''''''''
+++ /dev/null
-.. _guides/workflows:
-
-Workflows
-=========
-
-In OpenERP, a workflow is a technical artefact to manage a set of "things to
-do" associated to the records of some data model. The workflow provides a
-higher- level way to organize the things to do on a record.
-
-More specifically, a workflow is a directed graph where the nodes are called
-"activities" and the arcs are called "transitions".
-
-- Activities define work that should be done within the OpenERP server, such
- as changing the state of some records, or sending emails.
-- Transitions control how the workflow progresses from activity to activity.
-
-In the definition of a workflow, one can attach conditions, signals, and
-triggers to transitions, so that the behavior of the workflow depends on user
-actions (such as clicking on a button), changes to records, or arbitrary
-Python code.
-
-Basics
-------
-
-Defining a workflow with data files is straightforward: a record "workflow" is
-given together with records for the activities and the transitions. For
-instance, here is a simple sequence of two activities defined in XML
-
-.. code-block:: xml
-
- <record id="test_workflow" model="workflow">
- <field name="name">test.workflow</field>
- <field name="osv">test.workflow.model</field>
- <field name="on_create">True</field>
- </record>
-
- <record id="activity_a" model="workflow.activity">
- <field name="wkf_id" ref="test_workflow"/>
- <field name="flow_start">True</field>
- <field name="name">a</field>
- <field name="kind">function</field>
- <field name="action">print_a()</field>
- </record>
- <record id="activity_b" model="workflow.activity">
- <field name="wkf_id" ref="test_workflow"/>
- <field name="flow_stop">True</field>
- <field name="name">b</field>
- <field name="kind">function</field>
- <field name="action">print_b()</field>
- </record>
-
- <record id="trans_a_b" model="workflow.transition">
- <field name="act_from" ref="activity_a"/>
- <field name="act_to" ref="activity_b"/>
- </record>
-
-A worfklow is always defined with respect to a particular model (the model is
-given by the attribute ``osv`` on the model ``workflow``). Methods specified
-in the activities or transitions will be called on that model.
-
-In the example code above, a workflow called "test_workflow" is created. It is
-made up of two activies, named "a" and "b", and one transition, going from "a"
-to "b".
-
-The first activity has its attribute ``flow_start`` set to ``True`` so that
-OpenERP knows where to start the workflow traversal after it is instanciated.
-Because ``on_create`` is set to True on the workflow record, the workflow is
-instanciated for each newly created record. (Otherwise, the workflow should be
-instanciated by other means, such as from some module Python code.)
-
-When the workflow is instanciated, it begins with activity "a". That activity
-is of kind ``function``, which means that the action ``print_a()`` is a method
-call on the model ``test.workflow`` (the usual ``cr, uid, ids, context``
-arguments are passed for you).
-
-The transition between "a" and "b" does not specify any condition. This means
-that the workflow instance immediately goes from "a" to "b" after "a" has been
-processed, and thus also processes activity "b".
-
-Transitions
------------
-
-Transitions provide the control structures to orchestrate a workflow. When an
-activity is completed, the workflow engine tries to get across transitions
-departing from the completed activity, towards the next activities. In their
-simplest form (as in the example above), they link activities sequentially:
-activities are processed as soon as the activities preceding them are
-completed.
-
-Instead of running all activities in one fell swoop, it is also possible to
-wait on transitions, going through them only when some criteria are met. The
-criteria are the conditions, the signals, and the triggers. They are detailed
-in the following sections.
-
-Conditions
-''''''''''
-
-When an activity has been completed, its outgoing transitions are inspected to
-determine whether it is possible for the workflow instance to proceed through
-them and reach the next activities. When only a condition is defined (i.e., no
-signal or trigger is defined), the condition is evaluated by OpenERP, and if
-it evaluates to ``True``, the worklfow instance progresses through the
-transition. If the condition is not met, it will be reevaluated every time
-the associated record is modified, or by an explicit method call to do it.
-
-By default, the attribute ``condition`` (i.e., the expression to be evaluated)
-is just "True", which trivially evaluates to ``True``. Note that the condition
-may be several lines long; in that case, the value of the last one determines
-whether the transition can be taken.
-
-In the condition evaluation environment, several symbols are conveniently
-defined (in addition to the OpenERP ``safe_eval`` environment):
-
-- all the model column names, and
-- all the browse record's attributes.
-
-Signals
-'''''''
-
-In addition to a condition, a transition can specify a signal name. When such
-a signal name is present, the transition is not taken directly, even if the
-condition evaluates to ``True``. Instead the transition blocks, waiting to be
-woken up.
-
-In order to wake up a transition with a defined signal name, the signal must
-be sent to the workflow instance. A common way to send a signal is to use a
-button in the user interface, using the element ``<button/>`` with the signal
-name as the attribute ``name`` of the button. Once the button is clicked, the
-signal is sent to the workflow instance of the current record.
-
-.. note:: The condition is still evaluated when the signal is sent to the
- workflow instance.
-
-Triggers
-''''''''
-
-With conditions that evaluate to ``False``, transitions are not taken (and
-thus the activity it leads to is not processed immediately). Still, the
-workflow instance can get new chances to progress across that transition by
-providing so-called triggers. The idea is that when the condition is not
-satisfied, triggers are recorded in database. Later, it is possible to wake up
-specifically the workflow instances that installed those triggers, offering
-them to reevaluate their transition conditions. This mechanism makes it
-cheaper to wake up workflow instances by targetting just a few of them (those
-that have installed the triggers) instead of all of them.
-
-Triggers are recorded in database as record IDs (together with the model name)
-and refer to the workflow instance waiting for those records. The transition
-definition provides a model name (attribute ``trigger_model``) and a Python
-expression (attribute ``trigger_expression``) that evaluates to a list of
-record IDs in the given model. Any of those records can wake up the workflow
-instance they are associated with.
-
-.. note:: triggers are not re-installed whenever the transition is re-tried.
-
-Splitting and joining transitions
-'''''''''''''''''''''''''''''''''
-
-When multiple transitions leave the same activity, or lead to the same
-activity, OpenERP provides some control over which transitions are actually
-taken, or how the reached activity will be processed. The attributes
-``split_mode`` and ``join_mode`` on the activity are used for such
-control. The possible values of those attributes are explained below.
-
-Activities
-----------
-
-While the transitions can be seen as the control structures of the workflows,
-activities are the places where everything happens, from changing record
-states to sending email.
-
-Different kinds of activities exist: ``Dummy``, ``Function``, ``Subflow``, and
-``Stop all``, each doing different things when the activity is processed. In
-addition to their kind, activies have other properties, detailed in the next
-sections.
-
-Flow start and flow stop
-''''''''''''''''''''''''
-
-The attribute ``flow_start`` is a boolean value specifying whether the activity
-is processed when the workflow is instanciated. Multiple activities can have
-their attribute ``flow_start`` set to ``True``. When instanciating a workflow
-for a record, OpenERP simply processes all of them, and evaluate all their
-outgoing transitions afterwards.
-
-The attribute ``flow_stop`` is a boolean value specifying whether the activity
-stops the workflow instance. A workflow instance is considered completed when
-all its activities with the attribute ``flow_stop`` set to ``True`` are
-completed.
-
-It is important for OpenERP to know when a workflow instance is completed. A
-workflow can have an activity that is actually another workflow (called a
-subflow); that activity is completed when the subflow is completed.
-
-Subflow
-'''''''
-
-An activity can embed a complete workflow, called a subflow (the embedding
-workflow is called the parent workflow). The workflow to instanciate is
-specified by attribute ``subflow_id``.
-
-.. note:: In the GUI, that attribute can not be set unless the kind of the
- activity is ``Subflow``.
-
-The activity is considered completed (and its outgoing transitions ready to be
-evaluated) when the subflow is completed (see attribute ``flow_stop`` above).
-
-Sending a signal from a subflow
-'''''''''''''''''''''''''''''''
-
-When a workflow is embedded in an activity (as a subflow) of a workflow, the
-sublow can send a signal from its own activities to the parent workflow by
-giving a signal name in the attribute ``signal_send``. OpenERP processes those
-activities by sending the value of ``signal_send`` prefixed by "subflow." to
-the parent workflow instance.
-
-In other words, it is possible to react and get transitions in the parent
-workflow as activities are executed in the sublow.
-
-Server actions
-''''''''''''''
-
-An activity can run a "Server Action" by specifying its ID in the attribute
-``action_id``.
-
-Python action
-'''''''''''''
-
-An activity can execute some Python code, given by the attribute ``action``.
-The evaluation environment is the same as the one explained in the section
-`Conditions`_.
-
-Split mode
-''''''''''
-
-After an activity has been processed, its outgoing transitions are evaluated.
-Normally, if a transition can be taken, OpenERP traverses it and proceed to
-the activity the transition leads to.
-
-Actually, when more than a single transition is leaving an activity, OpenERP
-may proceed or not, depending on the other transitions. That is, the
-conditions on the transitions can be combined together, and the combined
-result instructs OpenERP to traverse zero, one, or all the transitions. The
-way they are combined is controlled by the attribute ``split_mode``.
-
-There are three possible split modes: ``XOR``, ``OR`` and ``AND``.
-
-``XOR``
- When the transitions are combined with a ``XOR`` split mode, as soon as a
- transition has a satisfied condition, the transition is traversed and the
- others are skipped.
-``OR``
- With the ``OR`` mode, all the transitions with a satisfied condition are
- traversed. The remaining transitions will not be evaluated later.
-``AND``
- With the ``AND`` mode, OpenERP will wait for all outgoing transition
- conditions to be satisfied, then traverse all of them at once.
-
-Join mode
-'''''''''
-
-Just like outgoing transition conditions can be combined together to decide
-whether they can be traversed or not, incoming transitions can be combined
-together to decide if and when an activity may be processed. The attribute
-``join_mode`` controls that behavior.
-
-There are two possible join modes: ``XOR`` and ``AND``.
-
-``XOR``
- With the ``XOR`` mode, an incoming transition with a satisfied condition
- is traversed immediately, and enables the processing of the activity.
-
-``AND``
- With the ``AND`` mode, OpenERP will wait until all incoming transitions
- have been traversed before enabling the processing of the activity.
-
-Kinds
-'''''
-
-Activities can be of different kinds: ``dummy``, ``function``, ``subflow``, or
-``stopall``. The kind defines what type of work an activity can do.
-
-Dummy
- The ``dummy`` kind is for activities that do nothing, or for activities
- that only call a server action. Activities that do nothing can be used as
- hubs to gather/dispatch transitions.
-Function
- The ``function`` kind is for activities that only need to run some Python
- code, and possibly a server action.
-Stop all
- The ``stopall`` kind is for activities that will completely stop the
- workflow instance and mark it as completed. In addition they can also run
- some Python code.
-Subflow
- When the kind of the activity is ``subflow``, the activity embeds another
- workflow instance. When the subflow is completed, the activity is also
- considered completed.
-
- By default, the subflow is instanciated for the same record as the parent
- workflow. It is possible to change that behavior by providing Python code
- that returns a record ID (of the same data model as the subflow). The
- embedded subflow instance is then the one of the given record.
--- /dev/null
+.. highlight:: xml
+
+===============
+Creating themes
+===============
+
+Basic set up
+============
+
+Create a basic theme module with :command:`odoo.py scaffold` and the ``theme``
+template: from the root odoo folder, use
+
+.. code-block:: console
+
+ $ ./odoo.py scaffold -t theme "Dummy Theme" addons
+
+this should create a new folder ``dummy_theme`` in the ``addons`` directory
+with the structure:
+
+.. code-block:: text
+
+ addons/dummy_theme
+ |-- __init__.py
+ |-- __openerp__.py
+ |-- static
+ | `-- style
+ | `-- custom.less
+ `-- views
+ |-- options.xml
+ |-- pages.xml
+ `-- snippets.xml
+
+``static/styles`` contains your stylesheet(s), ``views`` contains the various
+XML files describing the theme and theme features to Odoo.
+
+Static Page
+-----------
+
+Creating a new template
+'''''''''''''''''''''''
+
+Create a new file :file:`odoo/addons/theme_dummy/views/pages.xml` and open it.
+
+In odoo, a page means a new template. You don't need special skills, simply
+copy paste the lines::
+
+ <template id="website.hello" name="Homepage" page="True">
+ <t t-call="website.layout">
+ <div id="wrap" class="oe_structure oe_empty">
+ </div>
+ </t>
+ </template>
+
+Refresh the page and feel the hit.
+
+Editing content on a page
+'''''''''''''''''''''''''
+
+You can now add you content! You should always use the Bootstrap structure as
+below::
+
+ <template id="website.hello" name="Homepage" page="True">
+ <t t-call="website.layout">
+ <div id="wrap" class="oe_structure oe_empty">
+ <section>
+ <div class="container">
+ <div class="row">
+ <h1>This is Your Content</h1>
+ <p>Isn't amazing to edit everything inline?</p>
+ <hr/>
+ </div>
+ </div>
+ </section>
+ </div>
+ </t>
+ </template>
+
+Adding new item in the menu
+'''''''''''''''''''''''''''
+
+Adding these few more lines will put the new page in your menu::
+
+ <record id="hello_menu" model="website.menu">
+ <field name="name">Hello</field>
+ <field name="url">/page/hello</field>
+ <field name="parent_id" ref="website.main_menu"/>
+ <field name="sequence" type="int">20</field>
+ </record>
+
+Congrats! It's online! Now drag and drop some snippets on the page and let's
+style!
+
+Pimp Your Theme
+---------------
+
+Easy styling with less
+''''''''''''''''''''''
+
+In ``odoo/addons/theme_dummy/static`` create a new folder and name it
+``style``. In the new folder ``odoo/addons/theme_dummy/static/style`` create a
+file and name it ``custom.less``. Open ``custom.less`` in the text editor and
+modify these lines as below:
+
+
+.. code-block:: css
+
+ .h1 {
+ color: #215487;
+ }
+ .span {
+ border: 2px solid black;
+ background-color: #eee;
+ }
+
+Refresh the page and feel the hit.
+
+Get the most of the dom
+'''''''''''''''''''''''
+
+Right-Click, inspect element. You can go deeper by styling the main layout
+container. Here we try with the 'wrapwrap' id.
+
+.. code-block:: css
+
+ #wrapwrap {
+ background-color: #222;
+ width: 80%;
+ margin: 0 auto;
+ }
+
+Easy layout with bootstrap
+''''''''''''''''''''''''''
+
+Open :file:`odoo/addons/theme_dummy/views/pages.xml` and add a new section::
+
+ <section>
+ <div class="container">
+ <div class="row">
+ <div class="alert alert-primary" role="alert">
+ <a href="#" class="alert-link">...</a>
+ </div>
+ <div class="col-md-6 bg-blue">
+ <h2>BLUE it!</h2>
+ </div>
+ <div class="col-md-6 bg-green">
+ <h2>GREEN THAT!</h2>
+ </div>
+ </div>
+ </div>
+ </section>
+
+Refresh the page and check how it looks.
+
+The background of the alert component is the default Bootstrap primary color.
+The two other div your created have no custom styles applied yet. Open
+:file:`odoo/addons/theme_dummy/static/style/custom.less` and add these lines:
+
+.. code-block:: css
+
+ @brand-primary: #1abc9c;
+ @color-blue: #3498db;
+ @color-green: #2ecc71;
+
+ .bg-blue { background: @color-blue; }
+ .bg-green { background: @color-green; }
+
+ .h2 { color: white; }
+
+As you see, the default primary has changed and your new colors are shining!
+
+Build Your First Snippet
+------------------------
+
+Setting up __openerp__.py
+'''''''''''''''''''''''''
+
+Open ``__openerp__.py`` and add a new line as below:
+
+.. code-block:: python
+
+ {
+ 'name': 'Dummy Theme',
+ 'description': 'Dummy Theme',
+ 'category': 'Website',
+ 'version': '1.0',
+ 'author': 'OpenERP SA',
+ 'depends': ['website'],
+ 'data': [
+ 'views/snippets.xml',
+ ],
+ 'application': True,
+ }
+
+In ``odoo/addons/theme_learn/views`` create a new xml file, name it
+``snippets.xml`` and open it in a text editor
+
+Add your snippet in the menu
+''''''''''''''''''''''''''''
+
+Before typing your html code, you need to locate it in the WEBb. drop-down
+menu. In this case, we will add it at the end of the Structure section::
+
+ <template id="snippets" inherit_id="website.snippets" name="Clean Theme snippets">
+ <xpath expr="//div[@id='snippet_structure']" position="inside">
+ </xpath>
+ </template>
+
+Now open a new div, do not give it any id or classes. It will contain your
+snippet::
+
+ <xpath expr="//div[@id='snippet_structure']" position="inside">
+ <div>
+ </div>
+ </xpath>
+
+A thumbnail is also needed to create a more attractive link in the menu. You
+can use labels to focus on your themes snippets. Simply add a new div with
+the class ``oe_snippet_thumbnail`` and add your thumbnail image (100x79px)::
+
+ <xpath expr="//div[@id='snippet_structure']" position="inside">
+ <div>
+ <div class="oe_snippet_thumbnail">
+ <img class="oe_snippet_thumbnail_img" src="/theme_Dummy/static/img/blocks/block_title.png"/>
+ <span class="oe_snippet_thumbnail_title">SNIP IT!</span>
+ </div>
+ </div>
+ </xpath>
+
+And voila! Your new snippet is now ready to use. Just drag and drop it on your
+page to see it in action.
+
+The snippet body
+''''''''''''''''
+
+A snippet has to be in a section with the class ``oe_snippet_body`` to work
+correctly. As Odoo use the Bootstrap framework, you have use containers and
+rows to hold your content. Please refer the the Bootstrap documentation::
+
+ <xpath expr="//div[@id='snippet_structure']" position="inside">
+ <div>
+ <div class="oe_snippet_thumbnail">
+ <img class="oe_snippet_thumbnail_img" src="/theme_Dummy/static/img/blocks/block_title.png"/>
+ <span class="oe_snippet_thumbnail_title">SNIP IT!</span>
+ </div>
+
+ <section class="oe_snippet_body fw_categories">
+ <div class="container">
+ <div class="row">
+ </div>
+ </div>
+ </section>
+ </div>
+ </xpath>
+
+Inside your fresh new row, add some bootstraped contents::
+
+ <div class="col-md-12 text-center mt32 mb32">
+ <h2>A great Title</h2>
+ <h3 class="text-muted ">And a great subtitle too</h3>
+ <p>Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diam nonummy nibh euismod tincidunt ut laoreet dolore magna aliquam erat volutpat. </p>
+ </div>
+
+
+Adding images to your snippet
+'''''''''''''''''''''''''''''
+
+You can easely add images in your snippets simply by setting up css
+backgrounds images.
+
+In ``odoo/addons/theme_dummy/static/`` create a new folder and name it
+``img``. Put your images there, in sub-folders if needed. Open
+:file:`odoo/addons/theme_dummy/static/style/custom.less`, add these lines
+
+.. code-block:: css
+
+ @img-01: url("../img/img-boy.png");
+ .dummy-boy { background-image: @img-01; }
+
+ @img-02: url("../img/img-girl.png");
+ .dummy-girl { background-image: @img-02; }
+
+In :file:`odoo/addons/theme_dummy/views/pages.xml` change the correspondant
+lines as below::
+
+ <section>
+ <div class="container">
+ <div class="row dummy-bg">
+ <div class="alert alert-primary" role="alert">
+ <a href="#" class="alert-link">...</a>
+ </div>
+ <div class="col-md-6">
+ <h2>BLUE it!</h2>
+ <div class="dummy-boy">
+ </div>
+ </div>
+ <div class="col-md-6">
+ <h2>GREEN THAT!</h2>
+ <div class="dummy-girl">
+ </div>
+ </div>
+ </div>
+ </div>
+ </section>
+
+Your new snippet is now ready to use. Just drag and drop it on your page to
+see it in action.
+
+Advanced Customization
+======================
+
+Defining Your Theme Options
+---------------------------
+
+Understanding XPath
+'''''''''''''''''''
+
+As your stylesheets are running on the whole website, giving more option to
+your snippets and applying them independently will push your design
+forward. In ``odoo/addons/theme_dummy/views/`` create a new file, name it
+``options.xml`` and add these lines::
+
+ <template id="gourman_website_options_pattern" inherit_id="website.snippet_options">
+ <xpath expr="//div[@data-option='dummy_options']//ul" position="after">
+ </xpath>
+ </template>
+
+Explain xpath
+"""""""""""""
+
+.. TODO:: syntax not correct (see website examples)
+
+Your option menu is now correctly set in the database, you can create an new dropdown menu.
+
+Let's say yout want three options which will change the text color and the background.
+In option.xml, add these lines inside the xpath::
+
+ <li data-check_class="text-purple"><a>YOUR OPTION 1</a></li>
+ <li class="dropdown-submenu">
+ <a tabindex="-1" href="#">Your sub option</a>
+ <ul class="dropdown-menu">
+ <li data-select_class="bg-yellow"><a>YOUR OPTION 2</a></li>
+ <li data-select_class="text-light-bg-dark"><a>YOUR OPTION 3</a></li>
+ <li data-select_class=""><a>None</a></li>
+ </ul>
+ <li>
+
+Simple less css options
+'''''''''''''''''''''''
+
+In order to see these options in action, you have to write some new css
+classes. Open custom.css and add this new lines
+
+.. code-block:: css
+
+ @color-purple: #2ecc71;
+ @color-yellow: #2ecc71;
+
+ .text-purple { color: @color-purple; }
+ .bg-yellow { background-color: @color-yellow;}
+ .text-light-bg-dark { color: #eee; background-color: #222;}
+
+Refresh the page. Select a snippet and click Customize. Choose one of your new
+options apply it.
+
+XPath & inherits
+''''''''''''''''
+
+You can also add images in your variables and use them on certain part of your
+pages, snippets or any html element.
+
+In :file:`odoo/addons/theme_dummy/static/style/custom.css` add these new lines
+
+.. code:: css
+
+ @bg-01: url("../img/background/bg-blur.jpg");
+
+ .bg-01 {
+ background-image: @bg-01;
+ }
+
+Now that you have set the background image, you can decide how and where the
+user can use it, for example, on a simple div.
+
+Open :file:`odoo/addons/theme_dummy/views/options.xml` and add this new xpath::
+
+ <xpath expr="//div[@data-option='background-dummy']//ul" position="after">
+ <ul class="dropdown-menu">
+ <li data-value="bg-01">
+ <a>Image 1</a>
+ </li>
+ </ul>
+ </xpath>
+
+Your option is ready to be applied but you want it to be shown only a certain
+part of a snippet.
+
+Open :file:`odoo/addons/theme_dummy/views/snippets.xml` and add a new snippet
+with the method we learned previously::
+
+ <xpath expr="//div[@id='snippet_structure']" position="inside">
+ <div>
+ <!-- Add a Thumbnail in the Website Builder drop-down menu -->
+ <div class="oe_snippet_thumbnail">
+ <img class="oe_snippet_thumbnail_img" src="/theme_Dummy/static/img/blocks/block_title.png"/>
+ <span class="oe_snippet_thumbnail_title">Test OPTION</span>
+ </div>
+ <!-- Your Snippet content -->
+ <section class="oe_snippet_body fw_categories">
+ <div class="container">
+ <div class="row">
+ <div class="col-md-6 text-center mt32 mb32">
+ <h2>NO OPTION</h2>
+ <p>OFF</p>
+ </div>
+ <div class="col-md-6 text-center mt32 mb32 test-option">
+ <h2>OPTION</h2>
+ <p>This div has the 'test-option' class</p>
+ </div>
+ </div>
+ </div>
+ </section>
+ </div>
+ </xpath>
+
+As you see, the second ``col-md`` has a class named ``test-option``. We are
+going to specify where this option can be turned on by adding the
+``data-selector`` attribute.
+
+Go back to your ``options.xml`` files, add these new lines::
+
+ <xpath expr="//div[@data-option='background-dummy']" position="attributes">
+ <attribute name="data-selector">test-option</attribute>
+ </xpath>
+
+Refresh your browser. You should now be able to add your image background on
+the left div only. The option is now available on each section but also on
+the left div with the custom class.
+
+The Image Database
+------------------
+
+Modifying the image database
+''''''''''''''''''''''''''''
+
+Odoo provides its own image library but you certainly want to adapt it to your
+design. Do not use the Media Manager uploading Tool to add image in your
+theme. The images url's will be lost on reload! Instead of uploading your
+images, you can create your own library and disable the old ones.
+
+In ``odoo/addons/theme_dummy/views/`` create a new file, name it
+``images.xml`` and add these lines::
+
+ <record id="image_bg_blue" model="ir.attachment">
+ <field name="name">bg_blue.jpg</field>
+ <field name="datas_fname">bg_blue.jpg</field>
+ <field name="res_model">ir.ui.view</field>
+ <field name="type">url</field>
+ <field name="url">/theme_clean/static/img/library/bg/bg_blue.jpg</field>
+ </record>
+
+Your images is now available in your Media Manager. And your Theme has a
+total new look.
+
+Theme Selector
+==============
+
+Set Up
+------
+
+Understanding theme variants
+''''''''''''''''''''''''''''
+
+Combining theme variants
+''''''''''''''''''''''''
* This guide assumes `basic knowledge of Python
<http://docs.python.org/2/tutorial/>`_
* This guide assumes an installed Odoo
- * For production deployment, see the :ref:`dedicated deployment guides
- <guides/deployment>`
Creating a basic module
=======================
* :doc:`tutorials`, aimed at introducing the primary areas of developing Odoo
modules
-* :doc:`guides`, didactic documents covering more specific and specialized
- areas of Odoo, trying to solve more specific problems
* :doc:`reference`, which ought be the complete and canonical documentation
for Odoo subsystems
* :doc:`modules`, documenting useful specialized modules and integration
:hidden:
tutorials
- guides
reference
modules
reference/javascript
reference/reports
+ reference/workflows
report_obj = self.env['report']
report = report_obj._get_report_from_name('<<module.reportname>>')
docargs = {
- 'doc_ids': ids,
+ 'doc_ids': self._ids,
'doc_model': report.model,
- 'docs': self.env[report.model].browse(ids),
+ 'docs': self,
}
return report_obj.render('<<module.reportname>>', docargs)
+.. highlight:: xml
+
.. _reference/views:
=====
.. _reference/views/structure:
-Structure
-=========
+Common Structure
+================
View objects expose a number of fields, they are optional unless specified
otherwise)
``priority`` also defines the order of application during :ref:`view
inheritance <reference/views/inheritance>`
``arch``
- the description of the view's layout, see
- :ref:`reference/views/architecture`
+ the description of the view's layout
``groups_id``
:class:`~openerp.fields.Many2many` field to the groups allowed to view/use
the current view
A view's specs are applied sequentially.
-.. _reference/views/architecture:
-
-Architecture structures
-=======================
-
-Although they are all expressed as XML and have common points (most commonly
-the presence of ``<field>`` elements), each view has its own ``arch``
-structure with a specific root elements, semantics and affordances.
-
-Most views accept the ``create``, ``edit`` and ``delete`` attributes on their
-root element, when applicable this is used to disable the corresponding action
-from the view (hide the relevant buttons or avoid displaying an interface to
-perform it). May be set to ``true`` or ``false``. Setting them to ``true``
-will override their auto-generation from access-rights.
-
.. _reference/views/list:
Lists
------
+=====
The root element of list views is ``<tree>``\ [#treehistory]_. The list view's
root can have the following attributes:
.. _reference/views/form:
Forms
------
+=====
Form views are used to display the data from a single record. Their root
element is ``<form>``. They are composed of regular HTML_ with additional
structural and semantic components.
Structural components
-'''''''''''''''''''''
+---------------------
Structural components provide structure or "visual" features with little
logic. They are used as elements or sets of elements in form views.
itself, generally used to display workflow buttons and status widgets
Semantic components
-'''''''''''''''''''
+-------------------
Semantic components tie into and allow interaction with the Odoo
system. Available semantic components are:
.. todo:: widgets?
+Business Views guidelines
+-------------------------
+
+.. sectionauthor:: Aline Preillon, Raphael Collet
+
+Business views are targeted at regular users, not advanced users. Examples
+are: Opportunities, Products, Partners, Tasks, Projects, etc.
+
+.. image:: forms/oppreadonly.png
+ :class: img-responsive
+
+In general, a business view is composed of
+
+1. a status bar on top (with technical or business flow),
+2. a sheet in the middle (the form itself),
+3. a bottom part with History and Comments.
+
+Technically, the new form views are structured as follows in XML::
+
+ <form>
+ <header> ... content of the status bar ... </header>
+ <sheet> ... content of the sheet ... </sheet>
+ <div class="oe_chatter"> ... content of the bottom part ... </div>
+ </form>
+
+The Status Bar
+''''''''''''''
+
+The purpose of the status bar is to show the status of the current record and
+the action buttons.
+
+.. image:: forms/status.png
+ :class: img-responsive
+
+The Buttons
+...........
+
+The order of buttons follows the business flow. For instance, in a sale order,
+the logical steps are:
+
+1. Send the quotation
+2. Confirm the quotation
+3. Create the final invoice
+4. Send the goods
+
+Highlighted buttons (in red by default) emphasize the logical next step, to
+help the user. It is usually the first active button. On the other hand,
+:guilabel:`cancel` buttons *must* remain grey (normal). For instance, in
+Invoice the button :guilabel:`Refund` must never be red.
+
+Technically, buttons are highlighted by adding the class "oe_highlight"::
+
+ <button class="oe_highlight" name="..." type="..." states="..."/>
+
+The Status
+..........
+
+Uses the ``statusbar`` widget, and shows the current state in red. States
+common to all flows (for instance, a sale order begins as a quotation, then we
+send it, then it becomes a full sale order, and finally it is done) should be
+visible at all times but exceptions or states depending on particular sub-flow
+should only be visible when current.
+
+.. image:: forms/status1.png
+ :class: img-responsive
+
+.. image:: forms/status2.png
+ :class: img-responsive
+
+The states are shown following the order used in the field (the list in a
+selection field, etc). States that are always visible are specified with the
+attribute ``statusbar_visible``.
+
+``statusbar_colors`` can be used to give a custom color to specific states.
+
+::
+
+ <field name="state" widget="statusbar"
+ statusbar_visible="draft,sent,progress,invoiced,done"
+ statusbar_colors="{'shipping_except':'red','waiting_date':'blue'}"/>
+
+The Sheet
+'''''''''
+
+All business views should look like a printed sheet:
+
+.. image:: forms/sheet.png
+ :class: img-responsive
+
+1. Elements inside a ``<form>`` or ``<page>`` do not define groups, elements
+ inside them are laid out according to normal HTML rules. They content can
+ be explicitly grouped using ``<group>`` or regular ``<div>`` elements.
+2. By default, the element ``<group>`` defines two columns inside, unless an
+ attribute ``col="n"`` is used. The columns have the same width (1/n th of
+ the group's width). Use a ``<group>`` element to produce a column of fields.
+3. To give a title to a section, add a ``string`` attribute to a ``<group>`` element::
+
+ <group string="Time-sensitive operations">
+
+ this replaces the former use of ``<separator string="XXX"/>``.
+4. The ``<field>`` element does not produce a label, except as direct children
+ of a ``<group>`` element\ [#backwards-compatibility]_. Use :samp:`<label
+ for="{field_name}>` to produce a label of a field.
+
+Sheet Headers
+.............
+
+Some sheets have headers with one or more fields, and the labels of those
+fields are only shown in edit mode.
+
+.. list-table::
+ :header-rows: 1
+
+ * - View mode
+ - Edit mode
+ * - .. image:: forms/header.png
+ :class: img-responsive
+ - .. image:: forms/header2.png
+ :class: img-responsive
+
+Use HTML text, ``<div>``, ``<h1>``, ``<h2>``… to produce nice headers, and
+``<label>`` with the class ``oe_edit_only`` to only display the field's label
+in edit mode. The class ``oe_inline`` will make fields inline (instead of
+blocks): content following the field will be displayed on the same line rather
+than on the line below it. The form above is produced by the following XML::
+
+ <label for="name" class="oe_edit_only"/>
+ <h1><field name="name"/></h1>
+
+ <label for="planned_revenue" class="oe_edit_only"/>
+ <h2>
+ <field name="planned_revenue" class="oe_inline"/>
+ <field name="company_currency" class="oe_inline oe_edit_only"/> at
+ <field name="probability" class="oe_inline"/> % success rate
+ </h2>
+
+Button Box
+..........
+
+Many relevant actions or links can be displayed in the form. For example, in
+Opportunity form, the actions "Schedule a Call" and "Schedule a Meeting" take
+an important place in the use of the CRM. Instead of placing them in the
+"More" menu, put them directly in the sheet as buttons (on the top right) to
+make them more visible and more easily accessible.
+
+.. image:: forms/header3.png
+ :class: img-responsive
+
+Technically, the buttons are placed inside a <div> to group them as a block on
+the right-hand side of the sheet.
+
+::
+
+ <div class="oe_button_box oe_right">
+ <button string="Schedule/Log Call" name="..." type="action"/>
+ <button string="Schedule Meeting" name="action_makeMeeting" type="object"/>
+ </div>
+
+Groups and Titles
+.................
+
+A column of fields is now produced with a ``<group>`` element, with an
+optional title.
+
+.. image:: forms/screenshot-03.png
+ :class: img-responsive
+
+::
+
+ <group string="Payment Options">
+ <field name="writeoff_amount"/>
+ <field name="payment_option"/>
+ </group>
+
+It is recommended to have two columns of fields on the form. For this, simply
+put the ``<group>`` elements that contain the fields inside a top-level
+``<group>`` element.
+
+To make :ref:`view extension <reference/views/inheritance>` simpler, it is
+recommended to put a ``name`` attribute on ``<group>`` elements, so new fields
+can easily be added at the right place.
+
+Special Case: Subtotals
+~~~~~~~~~~~~~~~~~~~~~~~
+
+Some classes are defined to render subtotals like in invoice forms:
+
+.. image:: forms/screenshot-00.png
+ :class: img-responsive
+
+::
+
+ <group class="oe_subtotal_footer">
+ <field name="amount_untaxed"/>
+ <field name="amount_tax"/>
+ <field name="amount_total" class="oe_subtotal_footer_separator"/>
+ <field name="residual" style="margin-top: 10px"/>
+ </group>
+
+Placeholders and Inline Fields
+..............................
+
+Sometimes field labels make the form too complex. One can omit field labels,
+and instead put a placeholder inside the field. The placeholder text is
+visible only when the field is empty. The placeholder should tell what to
+place inside the field, it *must not* be an example as they are often confused
+with filled data.
+
+One can also group fields together by rendering them "inline" inside an
+explicit block element like `<div>``. This allows grouping semantically
+related fields as if they were a single (composite) fields.
+
+The following example, taken from the *Leads* form, shows both placeholders and
+inline fields (zip and city).
+
+.. list-table::
+ :header-rows: 1
+
+ * - Edit mode
+ - View mode
+ * - .. image:: forms/placeholder.png
+ :class: img-responsive
+ - .. image:: forms/screenshot-01.png
+ :class: img-responsive
+
+::
+
+ <group>
+ <label for="street" string="Address"/>
+ <div>
+ <field name="street" placeholder="Street..."/>
+ <field name="street2"/>
+ <div>
+ <field name="zip" class="oe_inline" placeholder="ZIP"/>
+ <field name="city" class="oe_inline" placeholder="City"/>
+ </div>
+ <field name="state_id" placeholder="State"/>
+ <field name="country_id" placeholder="Country"/>
+ </div>
+ </group>
+
+Images
+......
+
+Images, like avatars, should be displayed on the right of the sheet. The
+product form looks like:
+
+.. image:: forms/screenshot-02.png
+ :class: img-responsive
+
+The form above contains a <sheet> element that starts with:
+
+::
+
+ <field name="product_image" widget="image" class="oe_avatar oe_right"/>
+
+Tags
+....
+
+Most :class:`~openerp.fields.Many2many` fields, like categories, are better
+rendered as a list of tags. Use the widget ``many2many_tags`` for this:
+
+.. image:: forms/screenshot-04.png
+ :class: img-responsive
+
+::
+
+ <field name="category_id" widget="many2many_tags"/>
+
+Configuration forms guidelines
+------------------------------
+
+Examples of configuration forms: Stages, Leave Type, etc. This concerns all
+menu items under Configuration of each application (like Sales/Configuration).
+
+.. image:: forms/nosheet.png
+ :class: img-responsive
+
+1. no header (because no state, no workflow, no button)
+2. no sheet
+
+Dialog forms guidelines
+-----------------------
+
+Example: "Schedule a Call" from an opportunity.
+
+.. image:: forms/wizard-popup.png
+ :class: img-responsive
+
+1. avoid separators (the title is already in the popup title bar, so another
+ separator is not relevant)
+2. avoid cancel buttons (user generally close the popup window to get the same
+ effect)
+3. action buttons must be highlighted (red)
+4. when there is a text area, use a placeholder instead of a label or a
+ separator
+5. like in regular form views, put buttons in the <header> element
+
+Configuration Wizards guidelines
+--------------------------------
+
+Example: Settings / Configuration / Sales.
+
+1. always in line (no popup)
+2. no sheet
+3. keep the cancel button (users cannot close the window)
+4. the button "Apply" must be red
+
+
.. _reference/views/graph:
Graphs
-------
+======
The graph view is used to visualize aggregations over a number of records or
record groups. Its root element is ``<graph>`` which can take the following
``name`` (required)
the name of a field to use in a graph view. If used for grouping (rather
- than aggregating), can be augmented with a
- :ref:`reference/views/graph/functions`
+ than aggregating)
``type``
indicates whether the field should be used as a grouping criteria or as an
``measure``
field to aggregate within a group
+``interval``
+ on date and datetime fields, groups by the specified interval (``day``,
+ ``week``, ``month``, ``quarter`` or ``year``) instead of grouping on the
+ specific datetime (fixed second resolution) or date (fixed day resolution).
+
.. warning::
graph view aggregations are performed on database content, non-stored
function fields can not be used in graph views
-.. _reference/views/graph/functions:
-
-Grouping function
-'''''''''''''''''
-
-Field names in graph views can be postfixed with a grouping function using the
-form :samp:`{field_name}:{function}`. As of 8.0, only date and datetime fields
-support grouping functions. The available grouping functions are ``day``,
-``week``, ``month``, ``quarter`` and ``year``. By default, date and datetime
-fields are grouped month-wise.
-
.. _reference/views/kanban:
Kanban
-------
+======
The kanban view is a `kanban board`_ visualisation: it displays records as
"cards", halfway between a :ref:`list view <reference/views/list>` and a
* kanban structures/widgets (vignette, details, ...)
Javascript API
-''''''''''''''
+--------------
.. js:class:: KanbanRecord
.. _reference/views/calendar:
Calendar
---------
+========
Calendar views display records as events in a daily, weekly or monthly
calendar. Their root element is ``<calendar>``. Available attributes on the
.. _reference/views/gantt:
Gantt
------
+=====
Gantt views appropriately display Gantt charts (for scheduling).
.. _reference/views/diagram:
Diagram
--------
+=======
The diagram view can be used to display directed graphs of records. The root
element is ``<diagram>`` and takes no attributes.
.. _reference/views/search:
Search
-------
+======
Search views are a break from previous view types in that they don't display
*content*: although they apply to a specific model, they are used to filter
as inclusively composited: they will be composed with ``OR`` rather
than the usual ``AND``, e.g.
- .. code-block:: xml
+ ::
<filter domain="[('state', '=', 'draft')]"/>
<filter domain="[('state', '=', 'done')]"/>
if both filters are selected, will select the records whose ``state``
is ``draft`` or ``done``, but
- .. code-block:: xml
+ ::
<filter domain="[('state', '=', 'draft')]"/>
<separator/>
.. _reference/views/search/defaults:
Search defaults
-'''''''''''''''
+---------------
Search fields and filters can be configured through the action's ``context``
using :samp:`search_default_{name}` keys. For fields, the value should be the
value to set in the field, for filters it's a boolean value. For instance,
-assuming ``foo`` is a field and ``bar`` is a filter an action context of::
+assuming ``foo`` is a field and ``bar`` is a filter an action context of:
+
+.. code-block:: python
{
'search_default_foo': 'acro',
.. _reference/views/qweb:
QWeb
-----
+====
QWeb views are standard :ref:`reference/qweb` templates inside a view's
``arch``. They don't have a specific root element.
:ref:`reference/data/template` should be used as a shortcut to define QWeb
views.
+.. [#backwards-compatibility] for backwards compatibility reasons
.. [#hasclass] an extension function is added for simpler matching in QWeb
views: ``hasclass(*classes)`` matches if the context node has
all the specified classes
--- /dev/null
+DOTFILES:=$(wildcard *.dot)
+SVGFILES:=$(patsubst %.dot,%.svg,$(DOTFILES))
+PNGFILES:=$(patsubst %.dot,%.png,$(DOTFILES))
+
+# try to disable implicit rules
+.SUFFIXES:
+
+.PHONY: all clean
+
+all: $(SVGFILES) $(PNGFILES)
+
+# must -f to ignore errors when running clean multiple times in a row
+clean:
+ rm -f *.png *.svg
+
+%.svg: %.dot
+ dot -Tsvg $< > $@
+
+%.png: %.dot
+ dot -Tpng $< > $@
--- /dev/null
+digraph join {
+ // dummy sources as support for edges, make invisible and height 0
+ a [style=invis height=0 fontsize=0]
+ b [style=invis height=0 fontsize=0]
+ c [style=invis height=0 fontsize=0]
+
+ a -> Activity
+ b -> Activity
+ c -> Activity
+}
--- /dev/null
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
+ "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<!-- Generated by graphviz version 2.38.0 (20140413.2041)
+ -->
+<!-- Title: join Pages: 1 -->
+<svg width="206pt" height="93pt"
+ viewBox="0.00 0.00 206.00 92.73" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 88.7279)">
+<title>join</title>
+<polygon fill="white" stroke="none" points="-4,4 -4,-88.7279 202,-88.7279 202,4 -4,4"/>
+<!-- a -->
+<!-- Activity -->
+<g id="node4" class="node"><title>Activity</title>
+<ellipse fill="none" stroke="black" cx="99" cy="-18" rx="40.0939" ry="18"/>
+<text text-anchor="middle" x="99" y="-14.3" font-family="Times,serif" font-size="14.00">Activity</text>
+</g>
+<!-- a->Activity -->
+<g id="edge1" class="edge"><title>a->Activity</title>
+<path fill="none" stroke="black" d="M33.6445,-71.9778C42.4332,-64.8537 58.4442,-51.875 72.4186,-40.5472"/>
+<polygon fill="black" stroke="black" points="74.7768,-43.1411 80.3411,-34.1251 70.3688,-37.7033 74.7768,-43.1411"/>
+</g>
+<!-- b -->
+<!-- b->Activity -->
+<g id="edge2" class="edge"><title>b->Activity</title>
+<path fill="none" stroke="black" d="M99,-71.9778C99,-66.0508 99,-56.0715 99,-46.3619"/>
+<polygon fill="black" stroke="black" points="102.5,-46.1364 99,-36.1364 95.5001,-46.1365 102.5,-46.1364"/>
+</g>
+<!-- c -->
+<!-- c->Activity -->
+<g id="edge3" class="edge"><title>c->Activity</title>
+<path fill="none" stroke="black" d="M164.355,-71.9778C155.567,-64.8537 139.556,-51.875 125.581,-40.5472"/>
+<polygon fill="black" stroke="black" points="127.631,-37.7033 117.659,-34.1251 123.223,-43.1411 127.631,-37.7033"/>
+</g>
+</g>
+</svg>
--- /dev/null
+digraph order {
+ Draft [style=filled fillcolor="#73fa79"]
+ Closed [style=filled fillcolor="#98c7df"]
+ Canceled [style=filled fillcolor="#98c7df"]
+
+ Draft -> Confirmed
+ Confirmed -> Closed
+ Confirmed -> Canceled
+}
--- /dev/null
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
+ "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<!-- Generated by graphviz version 2.38.0 (20140413.2041)
+ -->
+<!-- Title: order Pages: 1 -->
+<svg width="188pt" height="188pt"
+ viewBox="0.00 0.00 188.24 188.00" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 184)">
+<title>order</title>
+<polygon fill="white" stroke="none" points="-4,4 -4,-184 184.243,-184 184.243,4 -4,4"/>
+<!-- Draft -->
+<g id="node1" class="node"><title>Draft</title>
+<ellipse fill="#73fa79" stroke="black" cx="85.3968" cy="-162" rx="30.5947" ry="18"/>
+<text text-anchor="middle" x="85.3968" y="-158.3" font-family="Times,serif" font-size="14.00">Draft</text>
+</g>
+<!-- Confirmed -->
+<g id="node4" class="node"><title>Confirmed</title>
+<ellipse fill="none" stroke="black" cx="85.3968" cy="-90" rx="51.9908" ry="18"/>
+<text text-anchor="middle" x="85.3968" y="-86.3" font-family="Times,serif" font-size="14.00">Confirmed</text>
+</g>
+<!-- Draft->Confirmed -->
+<g id="edge1" class="edge"><title>Draft->Confirmed</title>
+<path fill="none" stroke="black" d="M85.3968,-143.697C85.3968,-135.983 85.3968,-126.712 85.3968,-118.112"/>
+<polygon fill="black" stroke="black" points="88.8969,-118.104 85.3968,-108.104 81.8969,-118.104 88.8969,-118.104"/>
+</g>
+<!-- Closed -->
+<g id="node2" class="node"><title>Closed</title>
+<ellipse fill="#98c7df" stroke="black" cx="36.3968" cy="-18" rx="36.2938" ry="18"/>
+<text text-anchor="middle" x="36.3968" y="-14.3" font-family="Times,serif" font-size="14.00">Closed</text>
+</g>
+<!-- Canceled -->
+<g id="node3" class="node"><title>Canceled</title>
+<ellipse fill="#98c7df" stroke="black" cx="135.397" cy="-18" rx="44.6926" ry="18"/>
+<text text-anchor="middle" x="135.397" y="-14.3" font-family="Times,serif" font-size="14.00">Canceled</text>
+</g>
+<!-- Confirmed->Closed -->
+<g id="edge2" class="edge"><title>Confirmed->Closed</title>
+<path fill="none" stroke="black" d="M73.7845,-72.411C67.8042,-63.8677 60.3917,-53.2785 53.7479,-43.7874"/>
+<polygon fill="black" stroke="black" points="56.5277,-41.6552 47.9257,-35.4699 50.7931,-45.6694 56.5277,-41.6552"/>
+</g>
+<!-- Confirmed->Canceled -->
+<g id="edge3" class="edge"><title>Confirmed->Canceled</title>
+<path fill="none" stroke="black" d="M97.2461,-72.411C103.348,-63.8677 110.912,-53.2785 117.692,-43.7874"/>
+<polygon fill="black" stroke="black" points="120.668,-45.6416 123.633,-35.4699 114.972,-41.573 120.668,-45.6416"/>
+</g>
+</g>
+</svg>
--- /dev/null
+digraph order {
+ Draft [style=filled fillcolor="#73fa79"]
+ Closed [style=filled fillcolor="#98c7df"]
+ Canceled [style=filled fillcolor="#98c7df"]
+
+ Draft -> Confirmed [label="discount <= 15%"]
+ Draft -> Validation [label="discount > 15%"]
+ Validation -> Confirmed [label="Accept"]
+ Confirmed -> Closed
+ Confirmed -> Canceled
+}
--- /dev/null
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
+ "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<!-- Generated by graphviz version 2.38.0 (20140413.2041)
+ -->
+<!-- Title: order Pages: 1 -->
+<svg width="260pt" height="291pt"
+ viewBox="0.00 0.00 259.79 291.00" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 287)">
+<title>order</title>
+<polygon fill="white" stroke="none" points="-4,4 -4,-287 255.792,-287 255.792,4 -4,4"/>
+<!-- Draft -->
+<g id="node1" class="node"><title>Draft</title>
+<ellipse fill="#73fa79" stroke="black" cx="85.3968" cy="-265" rx="30.5947" ry="18"/>
+<text text-anchor="middle" x="85.3968" y="-261.3" font-family="Times,serif" font-size="14.00">Draft</text>
+</g>
+<!-- Confirmed -->
+<g id="node4" class="node"><title>Confirmed</title>
+<ellipse fill="none" stroke="black" cx="85.3968" cy="-91" rx="51.9908" ry="18"/>
+<text text-anchor="middle" x="85.3968" y="-87.3" font-family="Times,serif" font-size="14.00">Confirmed</text>
+</g>
+<!-- Draft->Confirmed -->
+<g id="edge1" class="edge"><title>Draft->Confirmed</title>
+<path fill="none" stroke="black" d="M72.5816,-248.399C62.7132,-235.312 49.8641,-215.656 44.3968,-196 40.1092,-180.585 40.1092,-175.415 44.3968,-160 48.7071,-144.503 57.6054,-129.007 66.0142,-116.73"/>
+<polygon fill="black" stroke="black" points="68.9995,-118.572 71.9779,-108.405 63.3089,-114.496 68.9995,-118.572"/>
+<text text-anchor="middle" x="94.3968" y="-174.3" font-family="Times,serif" font-size="14.00">discount <= 15%</text>
+</g>
+<!-- Validation -->
+<g id="node5" class="node"><title>Validation</title>
+<ellipse fill="none" stroke="black" cx="202.397" cy="-178" rx="49.2915" ry="18"/>
+<text text-anchor="middle" x="202.397" y="-174.3" font-family="Times,serif" font-size="14.00">Validation</text>
+</g>
+<!-- Draft->Validation -->
+<g id="edge2" class="edge"><title>Draft->Validation</title>
+<path fill="none" stroke="black" d="M103.936,-250.531C122.48,-237.059 151.286,-216.131 172.959,-200.386"/>
+<polygon fill="black" stroke="black" points="175.139,-203.129 181.172,-194.42 171.024,-197.466 175.139,-203.129"/>
+<text text-anchor="middle" x="195.897" y="-217.8" font-family="Times,serif" font-size="14.00">discount > 15%</text>
+</g>
+<!-- Closed -->
+<g id="node2" class="node"><title>Closed</title>
+<ellipse fill="#98c7df" stroke="black" cx="36.3968" cy="-18" rx="36.2938" ry="18"/>
+<text text-anchor="middle" x="36.3968" y="-14.3" font-family="Times,serif" font-size="14.00">Closed</text>
+</g>
+<!-- Canceled -->
+<g id="node3" class="node"><title>Canceled</title>
+<ellipse fill="#98c7df" stroke="black" cx="135.397" cy="-18" rx="44.6926" ry="18"/>
+<text text-anchor="middle" x="135.397" y="-14.3" font-family="Times,serif" font-size="14.00">Canceled</text>
+</g>
+<!-- Confirmed->Closed -->
+<g id="edge4" class="edge"><title>Confirmed->Closed</title>
+<path fill="none" stroke="black" d="M73.7845,-73.174C67.7175,-64.3831 60.1766,-53.4564 53.4596,-43.7236"/>
+<polygon fill="black" stroke="black" points="56.1491,-41.4588 47.5884,-35.2165 50.3879,-45.4348 56.1491,-41.4588"/>
+</g>
+<!-- Confirmed->Canceled -->
+<g id="edge5" class="edge"><title>Confirmed->Canceled</title>
+<path fill="none" stroke="black" d="M97.2461,-73.174C103.348,-64.5087 110.912,-53.7682 117.692,-44.1415"/>
+<polygon fill="black" stroke="black" points="120.736,-45.8965 123.633,-35.7052 115.013,-41.8661 120.736,-45.8965"/>
+</g>
+<!-- Validation->Confirmed -->
+<g id="edge3" class="edge"><title>Validation->Confirmed</title>
+<path fill="none" stroke="black" d="M181.209,-161.607C162.688,-148.152 135.604,-128.475 114.953,-113.472"/>
+<polygon fill="black" stroke="black" points="116.97,-110.612 106.822,-107.566 112.856,-116.275 116.97,-110.612"/>
+<text text-anchor="middle" x="170.397" y="-130.8" font-family="Times,serif" font-size="14.00">Accept</text>
+</g>
+</g>
+</svg>
--- /dev/null
+digraph split {
+ // dummy destinations as support for edges, make invisible and height 0
+ a [style=invis height=0 fontsize=0]
+ b [style=invis height=0 fontsize=0]
+ c [style=invis height=0 fontsize=0]
+
+ Activity -> a
+ Activity -> b
+ Activity -> c
+}
--- /dev/null
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
+ "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<!-- Generated by graphviz version 2.38.0 (20140413.2041)
+ -->
+<!-- Title: split Pages: 1 -->
+<svg width="206pt" height="93pt"
+ viewBox="0.00 0.00 206.00 92.73" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 88.7279)">
+<title>split</title>
+<polygon fill="white" stroke="none" points="-4,4 -4,-88.7279 202,-88.7279 202,4 -4,4"/>
+<!-- a -->
+<!-- b -->
+<!-- c -->
+<!-- Activity -->
+<g id="node4" class="node"><title>Activity</title>
+<ellipse fill="none" stroke="black" cx="99" cy="-66.7279" rx="40.0939" ry="18"/>
+<text text-anchor="middle" x="99" y="-63.0279" font-family="Times,serif" font-size="14.00">Activity</text>
+</g>
+<!-- Activity->a -->
+<g id="edge1" class="edge"><title>Activity->a</title>
+<path fill="none" stroke="black" d="M80.4582,-50.6978C68.3594,-40.8903 52.7894,-28.2691 41.577,-19.1802"/>
+<polygon fill="black" stroke="black" points="43.641,-16.3478 33.6687,-12.7696 39.233,-21.7857 43.641,-16.3478"/>
+</g>
+<!-- Activity->b -->
+<g id="edge2" class="edge"><title>Activity->b</title>
+<path fill="none" stroke="black" d="M99,-48.5325C99,-40.494 99,-30.9869 99,-23.1351"/>
+<polygon fill="black" stroke="black" points="102.5,-22.8941 99,-12.8941 95.5001,-22.8942 102.5,-22.8941"/>
+</g>
+<!-- Activity->c -->
+<g id="edge3" class="edge"><title>Activity->c</title>
+<path fill="none" stroke="black" d="M117.542,-50.6978C129.641,-40.8903 145.211,-28.2691 156.423,-19.1802"/>
+<polygon fill="black" stroke="black" points="158.767,-21.7857 164.331,-12.7696 154.359,-16.3478 158.767,-21.7857"/>
+</g>
+</g>
+</svg>
--- /dev/null
+.. _reference/workflows:
+
+Workflows
+=========
+
+In Odoo, a workflow is a technical artefact to manage a set of "things to
+do" associated to the records of a model. The workflow provides a higher-level
+way to organize tasks to perform with or on a record.
+
+More specifically, a workflow is a directed graph where the nodes are called
+"activities" and the arcs are called "transitions".
+
+- Activities define work that should be done within the Odoo server, such
+ as changing the state of some records, or sending emails.
+- Transitions control how the workflow progresses from activity to activity.
+
+In the definition of a workflow, one can attach conditions, signals, and
+triggers to transitions, so that the behavior of the workflow depends on user
+actions (such as clicking on a button), changes to records, or arbitrary
+Python code.
+
+All in all, Odoo's workflow system provides:
+
+* a description of the evolution of a record (document) over time
+* automatic actions based on various and flexible conditions
+* management of company roles and validation steps
+* management of interactions between objects
+* a visual representation of document flows through their lifecycle
+
+For instance, a basic order could have the following flow:
+
+.. sphinx.ext.graphviz would be nice, but it requires ``dot`` on any machine
+.. where the doc is compiled... otoh this is a pain in the ass because you
+.. need 2 compilation steps (dot -> image and rst -> html) every time
+
+.. image:: workflow/order_0.*
+ :align: center
+
+Orders start in the *Draft* state, can be *Confirmed* by a user, and then
+either shipped (*Closed*) or *Canceled*.
+
+A company using Odoo may want to add discount support to orders, where sales
+staff has discretionary discounting powers up to 15%, but manager validation
+is required for discounts beyond 15%. The workflow can be altered online to
+add the relevant steps without editing Python or XML files:
+
+.. image:: workflow/order_1.*
+ :align: center
+
+Because Activities can perform arbitrary actions, the *Validation* can
+automatically send a validation request to the relevant employee.
+
+.. note:: the order view needs to be modified to add an *Accept Discount*
+ button for managers
+
+Basics
+------
+
+Defining a workflow with data files is straightforward: a record "workflow" is
+given together with records for the activities and the transitions. For
+instance, here is a simple sequence of two activities defined in XML
+
+.. code-block:: xml
+
+ <record id="test_workflow" model="workflow">
+ <field name="name">test.workflow</field>
+ <field name="osv">test.workflow.model</field>
+ <field name="on_create">True</field>
+ </record>
+
+ <record id="activity_a" model="workflow.activity">
+ <field name="wkf_id" ref="test_workflow"/>
+ <field name="flow_start">True</field>
+ <field name="name">a</field>
+ <field name="kind">function</field>
+ <field name="action">print_a()</field>
+ </record>
+ <record id="activity_b" model="workflow.activity">
+ <field name="wkf_id" ref="test_workflow"/>
+ <field name="flow_stop">True</field>
+ <field name="name">b</field>
+ <field name="kind">function</field>
+ <field name="action">print_b()</field>
+ </record>
+
+ <record id="trans_a_b" model="workflow.transition">
+ <field name="act_from" ref="activity_a"/>
+ <field name="act_to" ref="activity_b"/>
+ </record>
+
+A worfklow is always defined with respect to a particular model (the model is
+given by the attribute ``osv`` on the model ``workflow``). Methods specified
+in the activities or transitions will be called on that model.
+
+In the example code above, a workflow called "test_workflow" is created. It is
+made up of two activies, named "a" and "b", and one transition, going from "a"
+to "b".
+
+The first activity has its attribute ``flow_start`` set to ``True`` so that
+Odoo knows where to start the workflow traversal after it is instanciated.
+Because ``on_create`` is set to True on the workflow record, the workflow is
+instanciated for each newly created record. (Otherwise, the workflow should be
+instanciated by other means, such as from some module Python code.)
+
+When the workflow is instanciated, it begins with activity "a". That activity
+is of kind ``function``, which means that the action ``print_a()`` is a method
+call on the model ``test.workflow`` (the usual ``cr, uid, ids, context``
+arguments are passed for you).
+
+The transition between "a" and "b" does not specify any condition. This means
+that the workflow instance immediately goes from "a" to "b" after "a" has been
+processed, and thus also processes activity "b".
+
+Activities
+----------
+
+While the transitions can be seen as the control structures of the workflows,
+activities are the places where everything happens, from changing record
+states to sending email.
+
+Different kinds of activities exist: ``Dummy``, ``Function``, ``Subflow``, and
+``Stop all``, each doing different things when the activity is processed. In
+addition to their kind, activies have other properties, detailed in the next
+sections.
+
+Flow start and flow stop
+''''''''''''''''''''''''
+
+The attribute ``flow_start`` is a boolean value specifying whether the activity
+is processed when the workflow is instanciated. Multiple activities can have
+their attribute ``flow_start`` set to ``True``. When instanciating a workflow
+for a record, Odoo simply processes all of them, and evaluate all their
+outgoing transitions afterwards.
+
+The attribute ``flow_stop`` is a boolean value specifying whether the activity
+stops the workflow instance. A workflow instance is considered completed when
+all its activities with the attribute ``flow_stop`` set to ``True`` are
+completed.
+
+It is important for Odoo to know when a workflow instance is completed. A
+workflow can have an activity that is actually another workflow (called a
+subflow); that activity is completed when the subflow is completed.
+
+Subflow
+'''''''
+
+An activity can embed a complete workflow, called a subflow (the embedding
+workflow is called the parent workflow). The workflow to instanciate is
+specified by attribute ``subflow_id``.
+
+.. note:: In the GUI, that attribute can not be set unless the kind of the
+ activity is ``Subflow``.
+
+The activity is considered completed (and its outgoing transitions ready to be
+evaluated) when the subflow is completed (see attribute ``flow_stop`` above).
+
+Sending a signal from a subflow
+'''''''''''''''''''''''''''''''
+
+When a workflow is embedded in an activity (as a subflow) of a workflow, the
+sublow can send a signal from its own activities to the parent workflow by
+giving a signal name in the attribute ``signal_send``. Odoo processes those
+activities by sending the value of ``signal_send`` prefixed by "subflow." to
+the parent workflow instance.
+
+In other words, it is possible to react and get transitions in the parent
+workflow as activities are executed in the sublow.
+
+Server actions
+''''''''''''''
+
+An activity can run a "Server Action" by specifying its ID in the attribute
+``action_id``.
+
+Python action
+'''''''''''''
+
+An activity can execute some Python code, given by the attribute ``action``.
+The evaluation environment is the same as the one explained in the section
+`Conditions`_.
+
+Split mode
+''''''''''
+
+After an activity has been processed, Odoo evaluates its transition to reach
+the next activity in the flow.
+
+However if an activity has more than one transition, Odoo must decide which
+activity or activities to follow.
+
+.. image:: workflow/split.*
+ :align: center
+
+This choice is controlled by the ``split_mode`` attribute:
+
+``XOR`` (default)
+ By default, Odoo will use the first transition (in ``sequence`` order)
+ whose condition is satisfied. All other transitions are ignored.
+``OR``
+ In ``OR`` mode, all transitions with a satisfied condition are traversed
+ simultanously. Transitions not yet valid will be ignored, even if they
+ become valid later.
+``AND``
+ In ``AND`` mode, Odoo will wait until *all* transitions are satisfied, and
+ will traverse all of them (much like the ``OR`` mode).
+
+Both ``OR`` and ``AND`` mode will lead to activities being active in the same
+workflow.
+
+Join mode
+'''''''''
+
+Just like outgoing transition conditions can be combined together to decide
+whether they can be traversed or not, incoming transitions can be combined
+together to decide if and when an activity may be processed.
+
+.. image:: workflow/join.*
+ :align: center
+
+The ``join_mode`` attribute controls that behavior:
+
+``XOR`` (default)
+ Any incoming transition enables the activity and starts its processing.
+``AND``
+ The activity is enabled and processed only once *all* incoming transitions
+ have been traversed.
+
+Kinds
+'''''
+
+An activity's kind defines the type of work an activity can perform.
+
+Dummy (``dummy``, default)
+ Do nothing at all, or call a server action. Often used as dispatch or
+ gather "hubs" for transitions.
+Function (``function``)
+ Run some python code, execute a server action.
+Stop all (``stopall``)
+ Completely stops the workflow instance and marks it as completed.
+Subflow (``subflow``)
+ Starts executing an other workflow, once that workflow is completed the
+ activity is done processing.
+
+ By default, the subflow is instanciated for the same record as the parent
+ workflow. It is possible to change that behavior by providing Python code
+ that returns a record ID (of the same data model as the subflow). The
+ embedded subflow instance is then the one of the given record.
+
+
+Transitions
+-----------
+
+Transitions provide the control structures to orchestrate a workflow. When an
+activity is completed, the workflow engine tries to get across transitions
+departing from the completed activity, towards the next activities. In their
+simplest form (as in the example above), they link activities sequentially:
+activities are processed as soon as the activities preceding them are
+completed.
+
+Instead of running all activities in one fell swoop, it is also possible to
+wait on transitions, going through them only when some criteria are met. The
+criteria are the conditions, the signals, and the triggers. They are detailed
+in the following sections.
+
+Conditions
+''''''''''
+
+When an activity has been completed, its outgoing transitions are inspected to
+determine whether it is possible for the workflow instance to proceed through
+them and reach the next activities. When only a condition is defined (i.e., no
+signal or trigger is defined), the condition is evaluated by Odoo, and if
+it evaluates to ``True``, the worklfow instance progresses through the
+transition. If the condition is not met, it will be reevaluated every time
+the associated record is modified, or by an explicit method call to do it.
+
+By default, the attribute ``condition`` (i.e., the expression to be evaluated)
+is just "True", which trivially evaluates to ``True``. Note that the condition
+may be several lines long; in that case, the value of the last one determines
+whether the transition can be taken.
+
+In the condition evaluation environment, several symbols are conveniently
+defined (in addition to the Odoo ``safe_eval`` environment):
+
+- all the model column names, and
+- all the browse record's attributes.
+
+Signals
+'''''''
+
+In addition to a condition, a transition can specify a signal name. When such
+a signal name is present, the transition is not taken directly, even if the
+condition evaluates to ``True``. Instead the transition blocks, waiting to be
+woken up.
+
+In order to wake up a transition with a defined signal name, the signal must
+be sent to the workflow instance. A common way to send a signal is to use a
+button in the user interface, using the element ``<button/>`` with the signal
+name as the attribute ``name`` of the button. Once the button is clicked, the
+signal is sent to the workflow instance of the current record.
+
+.. note:: The condition is still evaluated when the signal is sent to the
+ workflow instance.
+
+Triggers
+''''''''
+
+With conditions that evaluate to ``False``, transitions are not taken (and
+thus the activity it leads to is not processed immediately). Still, the
+workflow instance can get new chances to progress across that transition by
+providing so-called triggers. The idea is that when the condition is not
+satisfied, triggers are recorded in database. Later, it is possible to wake up
+specifically the workflow instances that installed those triggers, offering
+them to reevaluate their transition conditions. This mechanism makes it
+cheaper to wake up workflow instances by targetting just a few of them (those
+that have installed the triggers) instead of all of them.
+
+Triggers are recorded in database as record IDs (together with the model name)
+and refer to the workflow instance waiting for those records. The transition
+definition provides a model name (attribute ``trigger_model``) and a Python
+expression (attribute ``trigger_expression``) that evaluates to a list of
+record IDs in the given model. Any of those records can wake up the workflow
+instance they are associated with.
+
+.. note:: triggers are not re-installed whenever the transition is re-tried.
howtos/website
howtos/backend
howtos/web
+ howtos/themes
``one2many`` and computed fields, including property fields and
related fields)
+ :param string oldname: the previous name of this field, so that ORM can rename
+ it automatically at migration
+
.. _field-computed:
.. rubric:: Computed fields
automatic = False # whether the field is automatically created ("magic" field)
inherited = False # whether the field is inherited (_inherits)
- column = None # the column interfaced by the field
+ column = None # the column corresponding to the field
setup_done = False # whether the field has been set up
name = None # name of the field
# by default, related fields are not stored
attrs['store'] = attrs.get('store', False)
+ # fix for function fields overridden by regular columns
+ if not isinstance(attrs.get('column'), (NoneType, fields.function)):
+ attrs.pop('store', None)
+
for attr, value in attrs.iteritems():
if not hasattr(self, attr):
self._free_attrs.append(attr)
setattr(self, attr, value)
- if not self.string:
+ if not self.string and not self.related:
+ # related fields get their string from their parent field
self.string = name.replace('_', ' ').capitalize()
# determine self.default and cls._defaults in a consistent way
self.depends = ('.'.join(self.related),)
self.compute = self._compute_related
self.inverse = self._inverse_related
- if field._description_searchable(env):
+ if field._description_searchable:
# allow searching on self only if the related field is searchable
self.search = self._search_related
return desc
# properties used by get_description()
-
- def _description_store(self, env):
- if self.store:
- # if the corresponding column is a function field, check the column
- column = env[self.model_name]._columns.get(self.name)
- return bool(getattr(column, 'store', True))
- return False
-
- def _description_searchable(self, env):
- if self.store:
- column = env[self.model_name]._columns.get(self.name)
- return bool(getattr(column, 'store', True)) or \
- bool(getattr(column, '_fnct_search', False))
- return bool(self.search)
-
- def _description_sortable(self, env):
- if self.store:
- column = env[self.model_name]._columns.get(self.name)
- return bool(getattr(column, 'store', True))
- if self.inherited:
- # self is sortable if the inherited field is itself sortable
- return self.related_field._description_sortable(env)
- return False
-
+ _description_store = property(attrgetter('store'))
_description_manual = property(attrgetter('manual'))
_description_depends = property(attrgetter('depends'))
_description_related = property(attrgetter('related'))
_description_change_default = property(attrgetter('change_default'))
_description_deprecated = property(attrgetter('deprecated'))
+ @property
+ def _description_searchable(self):
+ return bool(self.store or self.search or (self.column and self.column._fnct_search))
+
+ @property
+ def _description_sortable(self):
+ return self.store or (self.inherited and self.related_field._description_sortable)
+
def _description_string(self, env):
if self.string and env.lang:
name = "%s,%s" % (self.model_name, self.name)
def to_column(self):
""" return a low-level field object corresponding to `self` """
- assert self.store
+ assert self.store or self.column
# determine column parameters
_logger.debug("Create fields._column for Field %s", self)
# company-dependent fields are mapped to former property fields
args['type'] = self.type
args['relation'] = self.comodel_name
- return fields.property(**args)
-
- if self.column:
+ self.column = fields.property(**args)
+ elif self.column:
# let the column provide a valid column for the given parameters
- return self.column.new(**args)
+ self.column = self.column.new(**args)
+ else:
+ # create a fresh new column of the right type
+ self.column = getattr(fields, self.type)(**args)
- return getattr(fields, self.type)(**args)
+ return self.column
# properties used by to_column() to create a column instance
_column_copy = property(attrgetter('copy'))
""" Determine the value of `self` for `record`. """
env = record.env
- if self.store and not (self.depends and env.in_draft):
- # this is a stored field
+ if self.column and not (self.depends and env.in_draft):
+ # this is a stored field or an old-style function field
if self.depends:
# this is a stored computed field, check for recomputation
recs = record._recompute_check(self)
if self.digits:
assert isinstance(self.digits, (tuple, list)) and len(self.digits) >= 2, \
"Float field %s with digits %r, expecting (total, decimal)" % (self, self.digits)
- if self.store:
- column = env[self.model_name]._columns[self.name]
- column.digits_change(env.cr)
+ if self.column:
+ self.column.digits_change(env.cr)
def _setup_regular(self, env):
super(Float, self)._setup_regular(env)
def _setup_regular(self, env):
super(Many2many, self)._setup_regular(env)
- if self.store and not self.relation:
- model = env[self.model_name]
- column = model._columns[self.name]
- if not isinstance(column, fields.function):
- self.relation, self.column1, self.column2 = column._sql_names(model)
+ if not self.relation:
+ if isinstance(self.column, fields.many2many):
+ self.relation, self.column1, self.column2 = \
+ self.column._sql_names(env[self.model_name])
if self.relation:
m2m = env.registry._m2m
super(Id, self).__init__(type='integer', string=string, **kwargs)
def to_column(self):
- return fields.integer('ID')
+ self.column = fields.integer('ID')
+ return self.column
def __get__(self, record, owner):
if record is None:
def _handle_exception(self, exception):
"""Called within an except block to allow converting exceptions
- to abitrary responses. Anything returned (except None) will
+ to arbitrary responses. Anything returned (except None) will
be used as response."""
try:
return super(JsonRequest, self)._handle_exception(exception)
# basic setup of field
field.set_class_name(cls, name)
- if field.store:
+ if field.store or field.column:
cls._columns[name] = field.to_column()
else:
# remove potential column that may be overridden by field
if not partial:
raise
+ # update columns (fields may have changed), and column_infos
+ for name, field in self._fields.iteritems():
+ if field.column:
+ self._columns[name] = field.to_column()
+ self._inherits_reload()
+
# group fields by compute to determine field.computed_fields
fields_by_compute = defaultdict(list)
for field in self._fields.itervalues():
for key, val in vals.iteritems():
field = self._fields.get(key)
if field:
- if field.store or field.inherited:
+ if field.column or field.inherited:
old_vals[key] = val
if field.inverse and not field.inherited:
new_vals[key] = val
for key, val in vals.iteritems():
field = self._fields.get(key)
if field:
- if field.store or field.inherited:
+ if field.column or field.inherited:
old_vals[key] = val
if field.inverse and not field.inherited:
new_vals[key] = val
"""
from collections import Mapping
-from contextlib import contextmanager
import logging
+import os
import threading
import openerp
from .. import SUPERUSER_ID
-from openerp.tools import assertion_report, lazy_property
+from openerp.tools import assertion_report, lazy_property, classproperty, config
+from openerp.tools.lru import LRU
_logger = logging.getLogger(__name__)
registries (essentially database connection/model registry pairs).
"""
- # Mapping between db name and model registry.
- # Accessed through the methods below.
- registries = {}
+ _registries = None
_lock = threading.RLock()
_saved_lock = None
+ @classproperty
+ def registries(cls):
+ if cls._registries is None:
+ size = config.get('registry_lru_size', None)
+ if not size:
+ # Size the LRU depending of the memory limits
+ if os.name != 'posix':
+ # cannot specify the memory limit soft on windows...
+ size = 42
+ else:
+ # On average, a clean registry take 25MB of memory + cache
+ avgsz = 30 * 1024 * 1024
+ size = int(config['limit_memory_soft'] / avgsz)
+
+ cls._registries = LRU(size)
+ return cls._registries
+
@classmethod
def lock(cls):
""" Return the current registry lock. """
leaf.leaf = ('id', 'in', table_ids)
push(leaf)
- elif not field.store:
+ elif not column:
# Non-stored field should provide an implementation of search.
if not field.search:
# field does not support search!
def to_field_args(self):
args = super(function, self).to_field_args()
+ args['store'] = bool(self.store)
if self._type in ('float',):
args['digits'] = self.digits_compute or self.digits
elif self._type in ('selection', 'reference'):
time.sleep(SLEEP_INTERVAL + number) # Steve Reich timing style
registries = openerp.modules.registry.RegistryManager.registries
_logger.debug('cron%d polling for jobs', number)
- for db_name, registry in registries.items():
- while True and registry.ready:
+ for db_name, registry in registries.iteritems():
+ while registry.ready:
acquired = openerp.addons.base.ir.ir_cron.ir_cron._acquire_job(db_name)
if not acquired:
break
@locked
def borrow(self, dsn):
- self._debug('Borrow connection to %r', dsn)
-
# free dead and leaked connections
for i, (cnx, _) in tools.reverse_enumerate(self._connections):
if cnx.closed:
continue
self._connections.pop(i)
self._connections.append((cnx, True))
- self._debug('Existing connection found at index %d', i)
+ self._debug('Borrow existing connection to %r at index %d', cnx.dsn, i)
return cnx
@locked
def close_all(self, dsn=None):
- _logger.info('%r: Close all connections to %r', self, dsn)
+ count = 0
+ last = None
for i, (cnx, used) in tools.reverse_enumerate(self._connections):
if dsn is None or cnx._original_dsn == dsn:
cnx.close()
- self._connections.pop(i)
+ last = self._connections.pop(i)[0]
+ count += 1
+ _logger.info('%r: Closed %d connections %s', self, count,
+ (dsn and last and 'to %r' % last.dsn) or '')
class Connection(object):
#
##############################################################################
-__all__ = ['synchronized', 'lazy_property']
+__all__ = ['synchronized', 'lazy_property', 'classproperty']
from functools import wraps
from inspect import getsourcefile
return a(b(*args, **kwargs))
return wrapper
+
+class _ClassProperty(property):
+ def __get__(self, cls, owner):
+ return self.fget.__get__(None, owner)()
+
+def classproperty(func):
+ return _ClassProperty(classmethod(func))
+
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: