return {'value': res}
def unlink(self, cr, uid, ids, context=None):
+ statement_line_obj = self.pool['account.bank.statement.line']
for item in self.browse(cr, uid, ids, context=context):
if item.state != 'draft':
raise osv.except_osv(
_('Invalid Action!'),
_('In order to delete a bank statement, you must first cancel it to delete related journal items.')
)
+ # Explicitly unlink bank statement lines
+ # so it will check that the related journal entries have
+ # been deleted first
+ statement_line_obj.unlink(cr, uid, [line.id for line in item.line_ids], context=context)
return super(account_bank_statement, self).unlink(cr, uid, ids, context=context)
def button_journal_entries(self, cr, uid, ids, context=None):
'partner_id': fields.many2one('res.partner', 'Partner'),
'bank_account_id': fields.many2one('res.partner.bank','Bank Account'),
'account_id': fields.many2one('account.account', 'Account', help="This technical field can be used at the statement line creation/import time in order to avoid the reconciliation process on it later on. The statement line will simply create a counterpart on this account"),
- 'statement_id': fields.many2one('account.bank.statement', 'Statement', select=True, required=True, ondelete='cascade'),
+ 'statement_id': fields.many2one('account.bank.statement', 'Statement', select=True, required=True, ondelete='restrict'),
'journal_id': fields.related('statement_id', 'journal_id', type='many2one', relation='account.journal', string='Journal', store=True, readonly=True),
'partner_name': fields.char('Partner Name', help="This field is used to record the third party name when importing bank statement in electronic format, when the partner doesn't exist yet in the database (or cannot be found)."),
'ref': fields.char('Reference'),
@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)
'active': True,
}
+ def _check_country(self, cr, uid, ids, context=None):
+ obj = self.browse(cr, uid, ids[0], context=context)
+ if obj.country_id and obj.country_group_id:
+ return False
+ return True
+
+ _constraints = [
+ (_check_country, 'You can not select a country and a group of countries', ['country_id', 'country_group_id']),
+ ]
+
@api.v7
def map_tax(self, cr, uid, fposition_id, taxes, context=None):
if not taxes:
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
- break
- else:
+ if not tax_count:
result |= tax
return result
domain = [
('auto_apply', '=', True),
'|', ('vat_required', '=', False), ('vat_required', '=', partner.vat_subjected),
- '|', ('country_id', '=', None), ('country_id', '=', delivery.country_id.id),
- '|', ('country_group_id', '=', None), ('country_group_id.country_ids', '=', delivery.country_id.id)
]
- fiscal_position_ids = self.search(cr, uid, domain, context=context)
+
+ fiscal_position_ids = self.search(cr, uid, domain + [('country_id', '=', delivery.country_id.id)], context=context, limit=1)
+ if fiscal_position_ids:
+ return fiscal_position_ids[0]
+
+ fiscal_position_ids = self.search(cr, uid, domain + [('country_group_id.country_ids', '=', delivery.country_id.id)], context=context, limit=1)
+ if fiscal_position_ids:
+ return fiscal_position_ids[0]
+
+ fiscal_position_ids = self.search(cr, uid, domain + [('country_id', '=', None), ('country_group_id', '=', None)], context=context, limit=1)
if fiscal_position_ids:
return fiscal_position_ids[0]
return False
<field name="auto_apply"/>
<field name="sequence"/>
<field name="vat_required" attrs="{'readonly': [('auto_apply', '=', False)]}"/>
- <field name="country_id" attrs="{'readonly': ['|', ('country_group_id','!=',False), ('auto_apply', '=', False)]}" />
- <field name="country_group_id" attrs="{'readonly': ['|', ('country_id','!=',False), ('auto_apply', '=', False)]}"/>
+ <field name="country_id"/>
+ <field name="country_group_id"/>
</group>
<separator string="Taxes Mapping"/>
<field name="tax_ids" widget="one2many_list">
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':
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})
+@charset "utf-8"
+
@mixin radius($radius: 5px)
-moz-border-radius: $radius
-webkit-border-radius: $radius
// singleton
bus.bus = new bus.Bus();
return bus;
-})();
\ No newline at end of file
+})();
name="hr_timesheet_invoice.report_analyticprofit"
file="hr_timesheet_invoice.report_analyticprofit"
report_type="qweb-pdf"
+ menu="False"
string="Timesheet Profit"
/>
</data>
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)
</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>
lot_id = False
if wiz:
lot_id = wiz.lot_id.id
- new_moves = stock_mov_obj.action_consume(cr, uid, [produce_product.id], (subproduct_factor * production_qty_uom),
+ qty = min(subproduct_factor * production_qty_uom, produce_product.product_qty) #Needed when producing more than maximum quantity
+ new_moves = stock_mov_obj.action_consume(cr, uid, [produce_product.id], qty,
location_id=produce_product.location_id.id, restrict_lot_id=lot_id, context=context)
stock_mov_obj.write(cr, uid, new_moves, {'production_id': production_id}, context=context)
+ remaining_qty = subproduct_factor * production_qty_uom - qty
+ if remaining_qty: # In case you need to make more than planned
+ #consumed more in wizard than previously planned
+ extra_move_id = stock_mov_obj.copy(cr, uid, produce_product.id, default={'state': 'confirmed',
+ 'product_uom_qty': remaining_qty,
+ 'production_id': production_id}, context=context)
+ if extra_move_id:
+ stock_mov_obj.action_done(cr, uid, [extra_move_id], context=context)
+
if produce_product.product_id.id == production.product_id.id:
main_production_move = produce_product.id
if consume['product_id'] != raw_material_line.product_id.id:
continue
consumed_qty = min(remaining_qty, raw_material_line.product_qty)
- stock_mov_obj.action_consume(cr, uid, [raw_material_line.id], consumed_qty, raw_material_line.location_id.id, restrict_lot_id=consume['lot_id'], consumed_for=main_production_move, context=context)
+ stock_mov_obj.action_consume(cr, uid, [raw_material_line.id], consumed_qty, raw_material_line.location_id.id,
+ restrict_lot_id=consume['lot_id'], consumed_for=main_production_move, context=context)
remaining_qty -= consumed_qty
if remaining_qty:
#consumed more in wizard than previously planned
product = self.pool.get('product.product').browse(cr, uid, consume['product_id'], context=context)
extra_move_id = self._make_consume_line_from_data(cr, uid, production, product, product.uom_id.id, remaining_qty, False, 0, context=context)
if extra_move_id:
+ if consume['lot_id']:
+ stock_mov_obj.write(cr, uid, [extra_move_id], {'restrict_lot_id': consume['lot_id']}, context=context)
stock_mov_obj.action_done(cr, uid, [extra_move_id], context=context)
self.message_post(cr, uid, production_id, body=_("%s produced") % self._description, context=context)
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:
'active': fields.boolean('Active', help="If unchecked, it will allow you to hide the product without removing it."),
'color': fields.integer('Color Index'),
- 'is_product_variant': fields.function( _is_product_variant, type='boolean', string='Only one product variant'),
+ 'is_product_variant': fields.function( _is_product_variant, type='boolean', string='Is product variant'),
'attribute_line_ids': fields.one2many('product.attribute.line', 'product_tmpl_id', 'Product Attributes'),
'product_variant_ids': fields.one2many('product.product', 'product_tmpl_id', 'Products', required=True),
'product.product': (lambda self, cr, uid, ids, c=None: ids, [], 10),
}, select=True),
'attribute_value_ids': fields.many2many('product.attribute.value', id1='prod_id', id2='att_id', string='Attributes', readonly=True, ondelete='restrict'),
+ 'is_product_variant': fields.function( _is_product_variant_impl, type='boolean', string='Is product variant'),
# image: all image fields are base64 encoded and PIL-supported
'image_variant': fields.binary("Variant Image",
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
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],
<?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>
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)
'product_id': product.id,
'product_uom': product.uom_id.id,
'product_uom_qty': remaining_qty,
- 'name': _('Extra Move: ') + op.product_id.name,
+ 'name': _('Extra Move: ') + product.name,
'state': 'draft',
}
return res
},
check_session_id: function() {
var self = this;
- if (this.avoid_recursion || self.use_cors)
+ if (this.avoid_recursion)
return $.when();
if (this.session_id)
return $.when(); // we already have the session id
- if (this.override_session || ! this.origin_server) {
+ if (!this.use_cors && (this.override_session || ! this.origin_server)) {
// If we don't use the origin server we consider we should always create a new session.
// Even if some browsers could support cookies when using jsonp that behavior is
// not consistent and the browser creators are tending to removing that feature.
}
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]
})
jdata = simplejson.loads(data)
nbr_measures = jdata['nbr_measures']
workbook = xlwt.Workbook()
- worksheet = workbook.add_sheet(jdata['title'])
+ worksheet = workbook.add_sheet(jdata['title'][:30])
header_bold = xlwt.easyxf("font: bold on; pattern: pattern solid, fore_colour gray25;")
header_plain = xlwt.easyxf("pattern: pattern solid, fore_colour gray25;")
bold = xlwt.easyxf("font: bold on;")
response.data = data
else:
size = (max_w, max_h)
- img = image_resize_and_sharpen(image, size)
+ img = image_resize_and_sharpen(image, size, preserve_aspect_ratio=True)
image_save_for_web(img, response.stream, format=image.format)
# invalidate content-length computed by make_conditional as
# writing to response.stream does not do it (as of werkzeug 0.9.3)
// generate all the controls markup
var css = "box-sizing: border-box; position: absolute; background-color: #fff; border: 1px solid #ccc; width: 8px; height: 8px; margin-left: -4px; margin-top: -4px;";
transfo.$markup = $(''
- + '<div class="transfo-controls">'
+ + '<div class="transfo-container">'
+ + '<div class="transfo-controls">'
+ '<div style="cursor: crosshair; position: absolute; margin: -30px; top: 0; right: 0; padding: 1px 0 0 1px;" class="transfo-rotator">'
- + '<span class="fa-stack fa-lg">'
- + '<i class="fa fa-circle fa-stack-2x"></i>'
- + '<i class="fa fa-repeat fa-stack-1x fa-inverse"></i>'
- + '</span>'
+ + '<span class="fa-stack fa-lg">'
+ + '<i class="fa fa-circle fa-stack-2x"></i>'
+ + '<i class="fa fa-repeat fa-stack-1x fa-inverse"></i>'
+ + '</span>'
+ '</div>'
+ '<div style="' + css + 'top: 0%; left: 0%; cursor: nw-resize;" class="transfo-scaler-tl"></div>'
+ '<div style="' + css + 'top: 0%; left: 100%; cursor: ne-resize;" class="transfo-scaler-tr"></div>'
+ '<div style="' + css + 'top: 50%; left: 0%; cursor: w-resize;" class="transfo-scaler-ml"></div>'
+ '<div style="' + css + 'top: 50%; left: 100%; cursor: e-resize;" class="transfo-scaler-mr"></div>'
+ '<div style="' + css + 'border: 0; width: 0px; height: 0px; top: 50%; left: 50%;" class="transfo-scaler-mc"></div>'
+ + '</div>'
+ '</div>');
transfo.$center = transfo.$markup.find(".transfo-scaler-mc");
_bind($this, transfo);
_targetCss($this, transfo);
+ _stop_animation($this[0]);
}
function _overwriteOptions ($this, transfo, settings) {
transfo.settings = $.extend(transfo.settings, settings || {});
}
+ function _stop_animation (target) {
+ target.style.webkitAnimationPlayState = "paused";
+ target.style.animationPlayState = "paused";
+ target.style.webkitTransition = "none";
+ target.style.transition = "none";
+ }
+
function _setOptions ($this, transfo) {
var style = $this.attr("style") || "";
var transform = style.match(/transform\s*:([^;]+)/) ? style.match(/transform\s*:([^;]+)/)[1] : "";
transfo.settings.scaley= transform.indexOf('scaleY') != -1 ? parseFloat(transform.match(/scaleY\(([^)]+)\)/)[1]) : 1;
transfo.settings.style = style.replace(/[^;]*transform[^;]+/g, '').replace(/;+/g, ';');
+
$this.attr("style", transfo.settings.style);
+ _stop_animation($this[0]);
+ transfo.settings.pos = $this.offset();
transfo.settings.height = $this.innerHeight();
transfo.settings.width = $this.innerWidth();
}
transfo.settings.css = window.getComputedStyle($this[0], null);
- transfo.settings.pos = $this.offset();
transfo.settings.rotationStep = 5;
transfo.settings.hide = false;
}
transfo.$markup.off().on("mousedown", mousedown);
- transfo.$markup.find(">:not(.transfo-scaler-mc)").off().on("mousedown", mousedown);
+ transfo.$markup.find(".transfo-controls >:not(.transfo-scaler-mc)").off().on("mousedown", mousedown);
}
function _mouseDown($this, div, transfo, event) {
settings.scaley = Math.round(settings.scaley*100)/100;
_targetCss($this, transfo);
+ _stop_animation($this[0]);
return false;
}
_setCss($this, settings.style, settings);
- _setCss(transfo.$markup,
- "position: absolute;" +
- "top:" + settings.pos.top + "px;" +
- "left:" + settings.pos.left + "px;" +
+ transfo.$markup.css({
+ "position": "absolute",
+ "width": width + "px",
+ "height": height + "px",
+ "top": settings.pos.top + "px",
+ "left": settings.pos.left + "px"
+ });
+
+ var $controls = transfo.$markup.find('.transfo-controls');
+ _setCss($controls,
"width:" + width + "px;" +
"height:" + height + "px;" +
"cursor: move;",
settings);
- transfo.$markup.find(">").css("transform", "scaleX("+(1/settings.scalex)+") scaleY("+(1/settings.scaley)+")");
+
+ $controls.children().css("transform", "scaleX("+(1/settings.scalex)+") scaleY("+(1/settings.scaley)+")");
_showHide($this, transfo);
function _showHide ($this, transfo) {
transfo.$markup.css("z-index", transfo.settings.hide ? -1 : 1000);
if (transfo.settings.hide) {
- transfo.$markup.find(">").hide();
+ transfo.$markup.find(".transfo-controls > *").hide();
transfo.$markup.find(".transfo-scaler-mc").show();
} else {
- transfo.$markup.find(">").show();
+ transfo.$markup.find(".transfo-controls > *").show();
}
}
this.$target.transfo({
hide: true,
callback: function () {
- var pos = $(this).data("transfo").$center.offset();
+ var center = $(this).data("transfo").$markup.find('.transfo-scaler-mc').offset();
+ var $option = self.$overlay.find('.btn-group:first');
self.$overlay.css({
- 'top': pos.top,
- 'left': pos.left,
+ 'top': center.top - $option.height()/2,
+ 'left': center.left,
'position': 'absolute',
});
self.$overlay.find(".oe_overlay_options").attr("style", "width:0; left:0!important; top:0;");
'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>
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)
return values
- mandatory_billing_fields = ["name", "phone", "email", "street2", "city", "country_id", "zip"]
- optional_billing_fields = ["street", "state_id", "vat", "vat_subjected"]
- mandatory_shipping_fields = ["name", "phone", "street", "city", "country_id", "zip"]
- optional_shipping_fields = ["state_id"]
+ mandatory_billing_fields = ["name", "phone", "email", "street2", "city", "country_id"]
+ optional_billing_fields = ["street", "state_id", "vat", "vat_subjected", "zip"]
+ mandatory_shipping_fields = ["name", "phone", "street", "city", "country_id"]
+ optional_shipping_fields = ["state_id", "zip"]
def checkout_parse(self, address_type, data, remove_prefix=False):
""" data is a dict OR a partner browse record
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'):
display: block;
}
+.oe_website_sale input.js_quantity {
+ min-width: 48px;
+ text-align: center;
+}
+
/* ---- Publish managment and options ---- */
.oe_overlay_options .dropdown ul[name="size"] table {
margin-left: 20px;
.discount .oe_default_price
display: block
+.oe_website_sale input.js_quantity
+ min-width: 48px
+ text-align: center
+
/* ---- Publish managment and options ---- */
.oe_overlay_options
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")] || "" );
$parent.find(".oe_default_price:first .oe_currency_value").html( price_to_str(+$(this).data('lst_price')) );
$parent.find(".oe_price:first .oe_currency_value").html(price_to_str(+$(this).data('price')) );
- var $img = $(this).closest('tr.js_product, .oe_website_sale').find('span[data-oe-model^="product."][data-oe-type="image"] img, img.product_detail_img');
+ var $img = $(this).closest('tr.js_product, .oe_website_sale').find('span[data-oe-model^="product."][data-oe-type="image"] img:first, img.product_detail_img');
$img.attr("src", "/website/image/product.product/" + $(this).val() + "/image");
});
}
if (product_id) {
- var $img = $(this).closest('tr.js_product, .oe_website_sale').find('span[data-oe-model^="product."][data-oe-type="image"] img, img.product_detail_img');
+ var $img = $(this).closest('tr.js_product, .oe_website_sale').find('span[data-oe-model^="product."][data-oe-type="image"] img:first, img.product_detail_img');
$img.attr("src", "/website/image/product.product/" + product_id + "/image");
$img.parent().attr('data-oe-model', 'product.product').attr('data-oe-id', product_id)
.data('oe-model', 'product.product').data('oe-id', product_id);
<input type="text" name="city" class="form-control" t-att-value="checkout.get('city')"/>
</div>
<div t-attf-class="form-group #{error.get('zip') and 'has-error' or ''} col-lg-6">
- <label class="control-label" for="zip">Zip / Postal Code</label>
+ <label class="control-label" for="zip" style="font-weight: normal">Zip / Postal Code</label>
<input type="text" name="zip" class="form-control" t-att-value="checkout.get('zip')"/>
</div>
<div t-attf-class="form-group #{error.get('country_id') and 'has-error' or ''} col-lg-6">
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>
<input type="text" name="shipping_city" class="form-control" t-att-value="checkout.get('shipping_city', '')" t-att-readonly=" 'readonly' if shipping_id >= 0 else ''"/>
</div>
<div t-attf-class="form-group #{error.get('shipping_zip') and 'has-error' or ''} col-lg-6">
- <label class="control-label" for="shipping_zip">Zip / Postal Code</label>
+ <label class="control-label" for="shipping_zip" style="font-weight: normal">Zip / Postal Code</label>
<input type="text" name="shipping_zip" class="form-control" t-att-value="checkout.get('shipping_zip', '')" t-att-readonly=" 'readonly' if shipping_id >= 0 else ''"/>
</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>
$(".js_remove .js_items").addClass("hidden");
$(".js_remove .js_item").removeClass("hidden");
} else {
- $(".js_remove .js_items").removeClass("hidden").text($(".js_remove .js_items").text().replace(/[0-9.,]+/, qty));
+ $(".js_remove .js_items").removeClass("hidden").text($(".js_remove .js_items:first").text().replace(/[0-9.,]+/, qty));
$(".js_remove .js_item").addClass("hidden");
}
});
});
return false;
});
-
+
+
+ $('#cart_products input.js_quantity').change(function () {
+ var value = $(this).val();
+ var $next = $(this).closest('tr').next('.optional_product');
+ while($next.length) {
+ $next.find('.js_quantity').text(value);
+ $next = $next.next('.optional_product');
+ }
+ });
});
<a href="#" class="js_remove"><small>Remove from cart</small></a>
</span>
</td>
+ <t t-set="option_inc" t-value="option_inc+1"/>
</tr>
</tbody>
from sphinx.locale import admonitionlabels, l_
admonitionlabels['exercise'] = l_('Exercise')
+
+# monkeypatch PHP lexer to not require <?php
+from sphinx.highlighting import lexers
+from pygments.lexers.web import PhpLexer
+lexers['php'] = PhpLexer(startinline=True)
{%- endblock -%}
{%- block content -%}
- <div class="document-super">
+ <div class="document-super {% if meta is defined %}{{ meta.classes }}{% endif %}">
{{ super() }}
</div>
{%- endblock -%}
</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 %}
});
}, 100);
+ // lang switcher
+ function findSheet(pattern, fromSheet) {
+ if (fromSheet) {
+ for(var i=0; i<fromSheet.cssRules.length; ++i) {
+ var rule = fromSheet.cssRules[i];
+ if (rule.type !== CSSRule.IMPORT_RULE) { continue; }
+ if (pattern.test(rule.href)) {
+ return rule.styleSheet;
+ }
+ }
+ return null;
+ }
+ var sheets = document.styleSheets;
+ for(var j=0; j<sheets.length; ++j) {
+ var sheet = sheets[j];
+ if (pattern.test(sheet.href)) {
+ return sheet;
+ }
+ var subSheet;
+ if (subSheet = findSheet(pattern, sheet)) {
+ return subSheet;
+ }
+ }
+ return null;
+ }
+ function buildSwitcher(languages) {
+ var root = document.createElement('ul');
+ root.className = "switcher";
+ for(var i=0; i<languages.length; ++i) {
+ var item = document.createElement('li');
+ item.textContent = languages[i];
+ if (i === 0) {
+ item.className = "active";
+ }
+ root.appendChild(item);
+ }
+ return root;
+ }
+ if ($('div.document-super').hasClass('stripe')) { (function () {
+ var sheet = findSheet(/style\.css$/);
+ if (!sheet) { return; }
+
+ // collect languages
+ var languages = {};
+ $('div.switchable').each(function () {
+ var classes = this.className.split(/\s+/);
+ for (var i = 0; i < classes.length; ++i) {
+ var cls = classes[i];
+ if (!/^highlight-/.test(cls)) { continue; }
+ languages[cls.slice(10)] = true;
+ }
+ });
+
+ $(buildSwitcher(Object.keys(languages)))
+ .prependTo('div.documentwrapper')
+ .on('click', 'li', function (e) {
+ $(e.target).addClass('active')
+ .siblings().removeClass('active');
+ var id = e.target.textContent;
+ var lastIndex = sheet.cssRules.length - 1;
+ var content = sheet.cssRules[lastIndex].style.cssText;
+ var sel = [
+ '.stripe .only-', id, ', ',
+ '.stripe .highlight-', id, ' > .highlight'
+ ].join('');
+ sheet.deleteRule(lastIndex);
+ sheet.insertRule(sel + '{' + content + '}', lastIndex);
+ });
+ })(); }
+
// Config ZeroClipboard
ZeroClipboard.config({
moviePath: '_static/ZeroClipboard.swf',
display: none !important;
}
}
+* {
+ box-sizing: border-box;
+}
body {
+ overflow: auto;
position: relative;
}
.document-super {
*
* Generated via Pygments
*/
-.highlight {
- padding: 9px 14px;
- margin-bottom: 14px;
- background-color: #f7f7f9 !important;
- border: 1px solid #e1e1e8;
- border-radius: 4px;
-}
.highlight pre {
- color: #333;
- padding: 0 45px 0 0;
- margin-top: 0;
- margin-bottom: 0;
- background-color: transparent;
- border: 0;
+ padding: 4px;
+ font-size: 75%;
+ word-break: normal;
+ word-wrap: normal;
}
/*
* ZeroClipboard styles
position: relative;
display: none;
}
+@media (min-width: 768px) {
+ .zero-clipboard {
+ display: block;
+ }
+}
.btn-clipboard {
position: absolute;
top: 0;
background-color: #a24689;
border-color: #a24689;
}
-@media (min-width: 768px) {
- .zero-clipboard {
- display: block;
- }
-}
img.align-center {
display: block;
margin: 0 auto;
margin: 0;
padding: 0;
}
-pre {
- word-break: normal;
- word-wrap: normal;
-}
.descclassname {
opacity: 0.5;
}
+.stripe .section {
+ margin-bottom: 2em;
+}
+@media (min-width: 992px) {
+ .stripe .section > *,
+ .stripe .section > .force-left {
+ width: 49%;
+ float: left;
+ clear: left;
+ }
+ .stripe .section > .force-right,
+ .stripe .section > [class*=highlight] {
+ float: none;
+ clear: none;
+ margin-left: 51%;
+ }
+ .stripe .section > h1,
+ .stripe .section > h2,
+ .stripe .section > h3,
+ .stripe .section > h4,
+ .stripe .section > h5,
+ .stripe .section > h6 {
+ background-color: rgba(255, 255, 255, 0.7);
+ }
+ .stripe .section > h1,
+ .stripe .section > h2,
+ .stripe .section > h3,
+ .stripe .section > h4,
+ .stripe .section > h5,
+ .stripe .section > h6,
+ .stripe .section > .section {
+ position: relative;
+ width: auto;
+ float: none;
+ clear: both;
+ }
+ .stripe .bodywrapper {
+ position: relative;
+ }
+ .stripe .bodywrapper:before {
+ position: absolute;
+ top: 0;
+ bottom: 0;
+ left: 50%;
+ content: "";
+ width: 0;
+ border-left: 1px solid #777777;
+ }
+}
+.stripe .switcher {
+ color: white;
+ width: auto !important;
+ float: none !important;
+ position: fixed;
+ display: -webkit-flex;
+ display: flex;
+ -webkit-justify-content: flex-end;
+ justify-content: flex-end;
+ right: 0.5em;
+ list-style: none;
+ padding: 0;
+ margin: 0;
+ z-index: 5;
+}
+.stripe .switcher li {
+ background-color: #0f131a;
+ padding: 0.4em 1em;
+ border: 1px solid #333;
+ border-left-width: 0;
+ cursor: pointer;
+}
+.stripe .switcher li:first-child {
+ border-left-width: 1px;
+ border-radius: 5px 0 0 5px;
+}
+.stripe .switcher li:last-child {
+ border-radius: 0 5px 5px 0;
+}
+.stripe .switcher li:hover {
+ background-color: #222;
+}
+.stripe .switcher li.active {
+ background-color: #333;
+}
+.stripe [class*=only-],
+.stripe .switchable > .highlight {
+ display: none;
+}
+.stripe .only-python,
+.stripe .highlight-python > .highlight {
+ display: block;
+}
// indent level for various items list e.g. dl, fields lists, ...
@item-indent: 30px;
+* {
+ box-sizing: border-box;
+}
body {
+ overflow: auto;
position: relative;
}
*
* Generated via Pygments
*/
-
-.highlight {
- padding: 9px 14px;
- margin-bottom: 14px;
- background-color: #f7f7f9 !important;
- border: 1px solid #e1e1e8;
- border-radius: 4px;
-}
.highlight pre {
- color: #333;
- padding: 0 45px 0 0;
- margin-top: 0;
- margin-bottom: 0;
- background-color: transparent;
- border: 0;
+ padding: 4px;
+
+ font-size: 75%;
+ // code block lines should not wrap
+ word-break: normal;
+ word-wrap: normal;
}
/*
.zero-clipboard {
position: relative;
display: none;
+ @media (min-width: @screen-sm-min) {
+ display: block;
+ }
}
.btn-clipboard {
position: absolute;
border-color: @brand-primary;
}
-@media (min-width: 768px) {
- .zero-clipboard {
- display: block;
- }
-}
-
// rST styles
img.align-center {
display: block;
padding: 0;
}
-// code block lines should not wrap
-pre {
- word-break: normal;
- word-wrap: normal;
-}
-
// lighten js namespace/class name
.descclassname {
opacity: 0.5;
}
+
+// STRIPE-STYLE PAGES
+.stripe {
+ .section {
+ margin-bottom: 2em;
+ }
+
+ // === columning only on medium+ ===
+ @media (min-width: @screen-md-min) {
+ // column 1
+ .section > *,
+ .section > .force-left {
+ width: 49%;
+ float: left;
+ clear: left;
+ }
+ // column 2
+ .section > .force-right,
+ .section > [class*=highlight] {
+ float: none;
+ clear: none;
+ margin-left: 51%;
+ }
+ // fullwidth elements
+ .section > h1, .section > h2, .section > h3, .section > h4, .section > h5,
+ .section > h6 {
+ background-color: fadeout(@body-bg, 30%);
+ }
+ .section > h1, .section > h2, .section > h3, .section > h4, .section > h5,
+ .section > h6, .section > .section {
+ position: relative;
+ width: auto;
+ float: none;
+ clear: both;
+ }
+
+ .bodywrapper {
+ position: relative;
+ // middle separator
+ &:before {
+ position: absolute;
+ top: 0;
+ bottom: 0;
+ left: 50%;
+ content: "";
+ width: 0;
+ border-left: 1px solid @gray-light;
+ }
+ }
+ }
+
+ .switcher {
+ color: white;
+ width: auto !important;
+ float: none !important;
+
+ position: fixed;
+ display: -webkit-flex;
+ display: flex;
+ -webkit-justify-content: flex-end;
+ justify-content: flex-end;
+
+ right: 0.5em;
+ list-style: none;
+ padding: 0;
+ margin: 0;
+ z-index: 5;
+
+ li {
+ background-color: #0f131a;
+ padding: 0.4em 1em;
+ border: 1px solid #333;
+ border-left-width: 0;
+ cursor: pointer;
+ &:first-child {
+ border-left-width: 1px;
+ border-radius: 5px 0 0 5px;
+ }
+ &:last-child {
+ border-radius: 0 5px 5px 0;
+ }
+ &:hover {
+ background-color: #222;
+ }
+ &.active {
+ background-color: #333;
+ }
+ }
+ }
+
+ // === show/hide code snippets ===
+ [class*=only-],
+ .switchable > .highlight {
+ display: none;
+ }
+ // must be final rule of page
+ .only-python, .highlight-python > .highlight {
+ display: block;
+ }
+}
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,
.. toctree::
:titlesonly:
+ modules/api_integration
--- /dev/null
+:classes: stripe
+
+===========
+Odoo as API
+===========
+
+Odoo is mostly extended internally via modules, but much of its features and
+all of its data is also available from the outside for external analysis or
+integration with various tools. Part of the :ref:`reference/orm/model` API is
+easily available over XML-RPC_ and accessible from a variety of languages.
+
+.. Odoo XML-RPC idiosyncracies:
+ * uses multiple endpoint and a nested call syntax instead of a
+ "hierarchical" server structure (e.g. ``openerp.res.partner.read()``)
+ * uses its own own manual auth system instead of basic auth or sessions
+ (basic is directly supported the Python and Ruby stdlibs as well as
+ ws-xmlrpc, not sure about ripcord)
+ * own auth is inconvenient as (uid, password) have to be explicitly passed
+ into every call. Session would allow db to be stored as well
+ These issues are especially visible in Java, somewhat less so in PHP
+
+Connection and authentication
+=============================
+
+Configuration
+-------------
+
+If you already have an Odoo server installed, you can just use its
+parameters
+
+.. rst-class:: switchable
+
+ .. code-block:: python
+
+ url = <insert server URL>
+ db = <insert database name>
+ username = 'admin'
+ password = <insert password for your admin user (default: admin)>
+
+ .. code-block:: ruby
+
+ url = <insert server URL>
+ db = <insert database name>
+ username = "admin"
+ password = <insert password for your admin user (default: admin)>
+
+ .. code-block:: php
+
+ $url = <insert server URL>;
+ $db = <insert database name>;
+ $username = "admin";
+ $password = <insert password for your admin user (default: admin)>;
+
+ .. code-block:: java
+
+ final String url = <insert server URL>,
+ db = <insert database name>,
+ username = "admin",
+ password = <insert password for your admin user (default: admin)>;
+
+To make exploration simpler, you can also ask https://demo.odoo.com for a test
+database:
+
+.. rst-class:: switchable
+
+ .. code-block:: python
+
+ import xmlrpclib
+ info = xmlrpclib.ServerProxy('https://demo.odoo.com/start').start()
+ url, db, username, password = \
+ info['host'], info['database'], info['user'], info['password']
+
+ .. code-block:: ruby
+
+ require "xmlrpc/client"
+ info = XMLRPC::Client.new2('https://demo.odoo.com/start').call('start')
+ url, db, username, password = \
+ info['host'], info['database'], info['user'], info['password']
+
+ .. code-block:: php
+
+ require_once('ripcord.php');
+ $info = ripcord::client('https://demo.odoo.com/start')->start();
+ list($url, $db, $username, $password) =
+ array($info['host'], $info['database'], $info['user'], $info['password']);
+
+ .. code-block:: java
+
+ final XmlRpcClient client = new XmlRpcClient();
+
+ final XmlRpcClientConfigImpl start_config = new XmlRpcClientConfigImpl();
+ start_config.setServerURL(new URL("https://demo.odoo.com/start"));
+ final Map<String, String> info = (Map<String, String>)client.execute(
+ start_config, "start", Collections.emptyList());
+
+ final String url = info.get("host"),
+ db = info.get("database"),
+ username = info.get("user"),
+ password = info.get("password");
+
+.. rst-class:: force-right
+
+ .. note::
+ :class: only-php
+
+ These examples use the `Ripcord <https://code.google.com/p/ripcord/>`_
+ library, which provides a simple XML-RPC API. Ripcord requires that
+ `XML-RPC support be enabled
+ <http://php.net/manual/en/xmlrpc.installation.php>`_ in your PHP
+ installation.
+
+ Since calls are performed over
+ `HTTPS <http://en.wikipedia.org/wiki/HTTP_Secure>`_, it also requires that
+ the `OpenSSL extension
+ <http://php.net/manual/en/openssl.installation.php>`_ be enabled.
+
+ .. note::
+ :class: only-java
+
+ These examples use the `Apache XML-RPC library
+ <https://ws.apache.org/xmlrpc/>`_
+
+Logging in
+----------
+
+Odoo requires users of the API to be authenticated before being able to query
+much data.
+
+The ``xmlrpc/2/common`` endpoint provides meta-calls which don't require
+authentication, such as the authentication itself or fetching version
+information. To verify if the connection information is correct before trying
+to authenticate, the simplest call is to ask for the server's version. The
+authentication itself is done through the ``authenticate`` function and
+returns a user identifier (``uid``) used in authenticated calls instead of
+the login.
+
+.. rst-class:: switchable
+
+ .. code-block:: python
+
+ common = xmlrpclib.ServerProxy('{}/xmlrpc/2/common'.format(url))
+ common.version()
+
+ .. code-block:: ruby
+
+ common = XMLRPC::Client.new2("#{url}/xmlrpc/2/common")
+ common.call('version')
+
+ .. code-block:: php
+
+ $common = ripcord::client("$url/xmlrpc/2/common");
+ $common->version();
+
+ .. code-block:: java
+
+ final XmlRpcClientConfigImpl common_config = new XmlRpcClientConfigImpl();
+ common_config.setServerURL(new URL(String.format("%s/xmlrpc/2/common", url)));
+ client.execute(common_config, "version", Collections.emptyList());
+
+.. code-block:: json
+
+ {
+ "server_version": "8.0",
+ "server_version_info": [8, 0, 0, "final", 0],
+ "server_serie": "8.0",
+ "protocol_version": 1,
+ }
+
+.. rst-class:: switchable
+
+ .. code-block:: python
+
+ uid = common.authenticate(db, username, password, {})
+
+ .. code-block:: ruby
+
+ uid = common.call('authenticate', db, username, password, {})
+
+ .. code-block:: php
+
+ $uid = $common->authenticate($db, $username, $password, array());
+
+ .. code-block:: java
+
+ int uid = (int)client.execute(
+ common_config, "authenticate", Arrays.asList(
+ db, username, password, Collections.emptyMap()));
+
+Calling methods
+===============
+
+The second — and most generally useful — is ``xmlrpc/2/object`` which is used
+to call methods of odoo models via the ``execute_kw`` RPC function.
+
+Each call to ``execute_kw`` takes the following parameters:
+
+* the database to use, a string
+* the user id (retrieved through ``authenticate``), an integer
+* the user's password, a string
+* the model name, a string
+* the method name, a string
+* an array/list of parameters passed by position
+* a mapping/dict of parameters to pass by keyword (optional)
+
+.. rst-class:: force-right
+
+For instance to see if we can read the ``res.partner`` model we can call
+``check_access_rights`` with ``operation`` passed by position and
+``raise_exception`` passed by keyword (in order to get a true/false result
+rather than true/error):
+
+.. rst-class:: switchable
+
+ .. code-block:: python
+
+ models = xmlrpclib.ServerProxy('{}/xmlrpc/2/object'.format(url))
+ models.execute_kw(db, uid, password,
+ 'res.partner', 'check_access_rights',
+ ['read'], {'raise_exception': False})
+
+ .. code-block:: ruby
+
+ models = XMLRPC::Client.new2("#{url}/xmlrpc/2/object").proxy
+ models.execute_kw(db, uid, password,
+ 'res.partner', 'check_access_rights',
+ ['read'], {raise_exception: false})
+
+ .. code-block:: php
+
+ $models = ripcord::client("$url/xmlrpc/2/object");
+ $models->execute_kw($db, $uid, $password,
+ 'res.partner', 'check_access_rights',
+ array('read'), array('raise_exception' => false));
+
+ .. code-block:: java
+
+ final XmlRpcClient models = new XmlRpcClient() {{
+ setConfig(new XmlRpcClientConfigImpl() {{
+ setServerURL(new URL(String.format("%s/xmlrpc/2/object", url)));
+ }});
+ }};
+ models.execute("execute_kw", Arrays.asList(
+ db, uid, password,
+ "res.partner", "check_access_rights",
+ Arrays.asList("read"),
+ new HashMap() {{ put("raise_exception", false); }}
+ ));
+
+.. code-block:: json
+
+ true
+
+.. todo:: this should be runnable and checked
+
+List records
+------------
+
+Records can be listed and filtered via :meth:`~openerp.models.Model.search`.
+
+:meth:`~openerp.models.Model.search` takes a mandatory
+:ref:`domain <reference/orm/domains>` filter (possibly empty), and returns the
+database identifiers of all records matching the filter. To list customer
+companies for instance:
+
+.. rst-class:: switchable
+
+ .. code-block:: python
+
+ models.execute_kw(db, uid, password,
+ 'res.partner', 'search',
+ [[['is_company', '=', True], ['customer', '=', True]]])
+
+ .. code-block:: ruby
+
+ models.execute_kw(db, uid, password,
+ 'res.partner', 'search',
+ [[['is_company', '=', true], ['customer', '=', true]]])
+
+ .. code-block:: php
+
+ $domain = array(array('is_company', '=', true),
+ array('customer', '=', true));
+ $models->execute_kw($db, $uid, $password,
+ 'res.partner', 'search', array($domain));
+
+ .. code-block:: java
+
+ final List domain = Arrays.asList(
+ Arrays.asList("is_company", "=", true),
+ Arrays.asList("customer", "=", true));
+ Arrays.asList((Object[])models.execute("execute_kw", Arrays.asList(
+ db, uid, password,
+ "res.partner", "search",
+ Arrays.asList(domain)
+ )));
+
+.. code-block:: json
+
+ [7, 18, 12, 14, 17, 19, 8, 31, 26, 16, 13, 20, 30, 22, 29, 15, 23, 28, 74]
+
+Pagination
+''''''''''
+
+By default a research will return the ids of all records matching the
+condition, which may be a huge number. ``offset`` and ``limit`` parameters are
+available to only retrieve a subset of all matched records.
+
+.. rst-class:: switchable
+
+ .. code-block:: python
+
+ models.execute_kw(db, uid, password,
+ 'res.partner', 'search',
+ [[['is_company', '=', True], ['customer', '=', True]]],
+ {'offset': 10, 'limit': 5})
+
+ .. code-block:: ruby
+
+ models.execute_kw(db, uid, password,
+ 'res.partner', 'search',
+ [[['is_company', '=', true], ['customer', '=', true]]],
+ {offset: 10, limit: 5})
+
+ .. code-block:: php
+
+ $models->execute_kw($db, $uid, $password,
+ 'res.partner', 'search',
+ array($domain),
+ array('offset'=>10, 'limit'=>5));
+
+ .. code-block:: java
+
+ Arrays.asList((Object[])models.execute("execute_kw", Arrays.asList(
+ db, uid, password,
+ "res.partner", "search",
+ Arrays.asList(domain),
+ new HashMap() {{ put("offset", 10); put("limit", 5); }}
+ )));
+
+.. code-block:: json
+
+ [13, 20, 30, 22, 29]
+
+Count records
+-------------
+
+Rather than retrieve a possibly gigantic list of records and count them
+afterwards, :meth:`~openerp.models.Model.search_count` can be used to retrieve
+only the number of records matching the query. It takes the same
+:ref:`domain <reference/orm/domains>` filter as
+:meth:`~openerp.models.Model.search` and no other parameter.
+
+.. rst-class:: switchable
+
+ .. code-block:: python
+
+ models.execute_kw(db, uid, password,
+ 'res.partner', 'search_count',
+ [[['is_company', '=', True], ['customer', '=', True]]])
+
+ .. code-block:: ruby
+
+ models.execute_kw(db, uid, password,
+ 'res.partner', 'search_count',
+ [[['is_company', '=', true], ['customer', '=', true]]])
+
+ .. code-block:: php
+
+ $models->execute_kw($db, $uid, $password,
+ 'res.partner', 'search_count',
+ array($domain));
+
+ .. code-block:: java
+
+ (Integer)models.execute("execute_kw", Arrays.asList(
+ db, uid, password,
+ "res.partner", "search_count",
+ Arrays.asList(domain)
+ ));
+
+.. code-block:: json
+
+ 19
+
+.. warning::
+
+ calling ``search`` then ``search_count`` (or the other way around) may not
+ yield coherent results if other users are using the server: stored data
+ could have changed between the calls
+
+Read records
+------------
+
+Record data is accessible via the :meth:`~openerp.models.Model.read` method,
+which takes a list of ids (as returned by
+:meth:`~openerp.models.Model.search`) and optionally a list of fields to
+fetch. By default, it will fetch all the fields the current user can read,
+which tends to be a huge amount.
+
+.. rst-class:: switchable
+
+ .. code-block:: python
+
+ ids = models.execute_kw(db, uid, password,
+ 'res.partner', 'search',
+ [[['is_company', '=', True], ['customer', '=', True]]],
+ {'limit': 1})
+ [record] = models.execute_kw(db, uid, password,
+ 'res.partner', 'read', [ids])
+ # count the number of fields fetched by default
+ len(record)
+
+ .. code-block:: ruby
+
+ ids = models.execute_kw(db, uid, password,
+ 'res.partner', 'search',
+ [[['is_company', '=', true], ['customer', '=', true]]],
+ {limit: 1})
+ record = models.execute_kw(db, uid, password,
+ 'res.partner', 'read', [ids]).first
+ # count the number of fields fetched by default
+ record.length
+
+ .. code-block:: php
+
+ $ids = $models->execute_kw($db, $uid, $password,
+ 'res.partner', 'search',
+ array($domain),
+ array('limit'=>1));
+ $records = $models->execute_kw($db, $uid, $password,
+ 'res.partner', 'read', array($ids));
+ // count the number of fields fetched by default
+ count($records[0]);
+
+ .. code-block:: java
+
+ final List ids = Arrays.asList((Object[])models.execute(
+ "execute_kw", Arrays.asList(
+ db, uid, password,
+ "res.partner", "search",
+ Arrays.asList(domain),
+ new HashMap() {{ put("limit", 1); }})));
+ final Map record = (Map)((Object[])models.execute(
+ "execute_kw", Arrays.asList(
+ db, uid, password,
+ "res.partner", "read",
+ Arrays.asList(ids)
+ )
+ ))[0];
+ // count the number of fields fetched by default
+ record.size();
+
+.. code-block:: json
+
+ 121
+
+Conversedly, picking only three fields deemed interesting.
+
+.. rst-class:: switchable
+
+ .. code-block:: python
+
+ models.execute_kw(db, uid, password,
+ 'res.partner', 'read',
+ [ids], {'fields': ['name', 'country_id', 'comment']})
+
+ .. code-block:: ruby
+
+ models.execute_kw(db, uid, password,
+ 'res.partner', 'read',
+ [ids], {fields: %w(name country_id comment)})
+
+ .. code-block:: php
+
+ $models->execute_kw($db, $uid, $password,
+ 'res.partner', 'read',
+ array($ids),
+ array('fields'=>array('name', 'country_id', 'comment')));
+
+ .. code-block:: java
+
+ Arrays.asList((Object[])models.execute("execute_kw", Arrays.asList(
+ db, uid, password,
+ "res.partner", "read",
+ Arrays.asList(ids),
+ new HashMap() {{
+ put("fields", Arrays.asList("name", "country_id", "comment"));
+ }}
+ )));
+
+.. code-block:: json
+
+ [{"comment": false, "country_id": [21, "Belgium"], "id": 7, "name": "Agrolait"}]
+
+.. note:: even if the ``id`` field is not requested, it is always returned
+
+Listing record fields
+---------------------
+
+:meth:`~openerp.models.Model.fields_get` can be used to inspect
+a model's fields and check which ones seem to be of interest.
+
+Because
+it returns a great amount of meta-information (it is also used by client
+programs) it should be filtered before printing, the most interesting items
+for a human user are ``string`` (the field's label), ``help`` (a help text if
+available) and ``type`` (to know which values to expect, or to send when
+updating a record):
+
+.. rst-class:: switchable
+
+ .. code-block:: python
+
+ fields = models.execute_kw(db, uid, password, 'res.partner', 'fields_get', [])
+ # filter keys of field attributes for display
+ {field: {
+ k: v for k, v in attributes.iteritems()
+ if k in ['string', 'help', 'type']
+ }
+ for field, attributes in fields.iteritems()}
+
+ .. code-block:: ruby
+
+ fields = models.execute_kw(db, uid, password, 'res.partner', 'fields_get', [])
+ # filter keys of field attributes for display
+ fields.each {|k, v|
+ fields[k] = v.keep_if {|kk, vv| %w(string help type).include? kk}
+ }
+
+ .. code-block:: php
+
+ $fields_full = $models->execute_kw($db, $uid, $password,
+ 'res.partner', 'fields_get', array());
+ // filter keys of field attributes for display
+ $allowed = array_flip(array('string', 'help', 'type'));
+ $fields = array();
+ foreach($fields_full as $field => $attributes) {
+ $fields[$field] = array_intersect_key($attributes, $allowed);
+ }
+
+ .. code-block:: java
+
+ final Map<String, Map<String, Object>> fields =
+ (Map<String, Map<String, Object>>)models.execute("execute_kw", Arrays.asList(
+ db, uid, password,
+ "res.partner", "fields_get",
+ Collections.emptyList()));
+ // filter keys of field attributes for display
+ final List<String> allowed = Arrays.asList("string", "help", "type");
+ new HashMap<String, Map<String, Object>>() {{
+ for(Entry<String, Map<String, Object>> item: fields.entrySet()) {
+ put(item.getKey(), new HashMap<String, Object>() {{
+ for(Entry<String, Object> it: item.getValue().entrySet()) {
+ if (allowed.contains(it.getKey())) {
+ put(it.getKey(), it.getValue());
+ }
+ }
+ }});
+ }
+ }};
+
+.. code-block:: json
+
+ {
+ "ean13": {
+ "type": "char",
+ "help": "BarCode",
+ "string": "EAN13"
+ },
+ "property_account_position": {
+ "type": "many2one",
+ "help": "The fiscal position will determine taxes and accounts used for the partner.",
+ "string": "Fiscal Position"
+ },
+ "signup_valid": {
+ "type": "boolean",
+ "help": "",
+ "string": "Signup Token is Valid"
+ },
+ "date_localization": {
+ "type": "date",
+ "help": "",
+ "string": "Geo Localization Date"
+ },
+ "ref_companies": {
+ "type": "one2many",
+ "help": "",
+ "string": "Companies that refers to partner"
+ },
+ "sale_order_count": {
+ "type": "integer",
+ "help": "",
+ "string": "# of Sales Order"
+ },
+ "purchase_order_count": {
+ "type": "integer",
+ "help": "",
+ "string": "# of Purchase Order"
+ },
+
+Search and read
+---------------
+
+Because that is a very common task, Odoo provides a
+:meth:`~openerp.models.Model.search_read` shortcut which as its name notes is
+equivalent to a :meth:`~openerp.models.Model.search` followed by a
+:meth:`~openerp.models.Model.read`, but avoids having to perform two requests
+and keep ids around. Its arguments are similar to
+:meth:`~openerp.models.Model.search`'s, but it can also take a list of
+``fields`` (like :meth:`~openerp.models.Model.read`, if that list is not
+provided it'll fetch all fields of matched records):
+
+.. rst-class:: switchable
+
+ .. code-block:: python
+
+ models.execute_kw(db, uid, password,
+ 'res.partner', 'search_read',
+ [[['is_company', '=', True], ['customer', '=', True]]],
+ {'fields': ['name', 'country_id', 'comment'], 'limit': 5})
+
+ .. code-block:: ruby
+
+ models.execute_kw(db, uid, password,
+ 'res.partner', 'search_read',
+ [[['is_company', '=', true], ['customer', '=', true]]],
+ {fields: %w(name country_id comment), limit: 5})
+
+ .. code-block:: php
+
+ $models->execute_kw($db, $uid, $password,
+ 'res.partner', 'search_read',
+ array($domain),
+ array('fields'=>array('name', 'country_id', 'comment'), 'limit'=>5));
+
+ .. code-block:: java
+
+ Arrays.asList((Object[])models.execute("execute_kw", Arrays.asList(
+ db, uid, password,
+ "res.partner", "search_read",
+ Arrays.asList(domain),
+ new HashMap() {{
+ put("fields", Arrays.asList("name", "country_id", "comment"));
+ put("limit", 5);
+ }}
+ )));
+
+.. code-block:: json
+
+ [
+ {
+ "comment": false,
+ "country_id": [ 21, "Belgium" ],
+ "id": 7,
+ "name": "Agrolait"
+ },
+ {
+ "comment": false,
+ "country_id": [ 76, "France" ],
+ "id": 18,
+ "name": "Axelor"
+ },
+ {
+ "comment": false,
+ "country_id": [ 233, "United Kingdom" ],
+ "id": 12,
+ "name": "Bank Wealthy and sons"
+ },
+ {
+ "comment": false,
+ "country_id": [ 105, "India" ],
+ "id": 14,
+ "name": "Best Designers"
+ },
+ {
+ "comment": false,
+ "country_id": [ 76, "France" ],
+ "id": 17,
+ "name": "Camptocamp"
+ }
+ ]
+
+
+Create records
+--------------
+
+.. rst-class:: switchable
+
+ .. code-block:: python
+
+ id = models.execute_kw(db, uid, password, 'res.partner', 'create', [{
+ 'name': "New Partner",
+ }])
+
+ .. code-block:: ruby
+
+ id = models.execute_kw(db, uid, password, 'res.partner', 'create', [{
+ name: "New Partner",
+ }])
+
+ .. code-block:: php
+
+ $id = $models->execute_kw($db, $uid, $password,
+ 'res.partner', 'create',
+ array(array('name'=>"New Partner")));
+
+ .. code-block:: java
+
+ final Integer id = (Integer)models.execute("execute_kw", Arrays.asList(
+ db, uid, password,
+ "res.partner", "create",
+ Arrays.asList(new HashMap() {{ put("name", "New Partner"); }})
+ ));
+
+.. code-block:: json
+
+ 78
+
+Update records
+--------------
+
+.. rst-class:: switchable
+
+ .. code-block:: python
+
+ models.execute_kw(db, uid, password, 'res.partner', 'write', [[id], {
+ 'name': "Newer partner"
+ }])
+ # get record name after having changed it
+ models.execute_kw(db, uid, password, 'res.partner', 'name_get', [[id]])
+
+ .. code-block:: ruby
+
+ models.execute_kw(db, uid, password, 'res.partner', 'write', [[id], {
+ name: "Newer partner"
+ }])
+ # get record name after having changed it
+ models.execute_kw(db, uid, password, 'res.partner', 'name_get', [[id]])
+
+ .. code-block:: php
+
+ $models->execute_kw($db, $uid, $password, 'res.partner', 'write',
+ array(array($id), array('name'=>"Newer partner")));
+ // get record name after having changed it
+ $models->execute_kw($db, $uid, $password,
+ 'res.partner', 'name_get', array(array($id)));
+
+ .. code-block:: java
+
+ models.execute("execute_kw", Arrays.asList(
+ db, uid, password,
+ "res.partner", "write",
+ Arrays.asList(
+ Arrays.asList(id),
+ new HashMap() {{ put("name", "Newer Partner"); }}
+ )
+ ));
+ // get record name after having changed it
+ Arrays.asList((Object[])models.execute("execute_kw", Arrays.asList(
+ db, uid, password,
+ "res.partner", "name_get",
+ Arrays.asList(Arrays.asList(id))
+ )));
+
+.. code-block:: json
+
+ [[78, "Newer partner"]]
+
+Delete records
+--------------
+
+.. rst-class:: switchable
+
+ .. code-block:: python
+
+ models.execute_kw(db, uid, password, 'res.partner', 'unlink', [[id]])
+ # check if the deleted record is still in the database
+ models.execute_kw(db, uid, password,
+ 'res.partner', 'search', [[['id', '=', id]]])
+
+ .. code-block:: ruby
+
+ models.execute_kw(db, uid, password, 'res.partner', 'unlink', [[id]])
+ # check if the deleted record is still in the database
+ models.execute_kw(db, uid, password,
+ 'res.partner', 'search', [[['id', '=', id]]])
+
+ .. code-block:: php
+
+ $models->execute_kw($db, $uid, $password,
+ 'res.partner', 'unlink',
+ array(array($id)));
+ // check if the deleted record is still in the database
+ $models->execute_kw($db, $uid, $password,
+ 'res.partner', 'search',
+ array(array(array('id', '=', $id))));
+
+ .. code-block:: java
+
+ models.execute("execute_kw", Arrays.asList(
+ db, uid, password,
+ "res.partner", "unlink",
+ Arrays.asList(Arrays.asList(id))));
+ // check if the deleted record is still in the database
+ Arrays.asList((Object[])models.execute("execute_kw", Arrays.asList(
+ db, uid, password,
+ "res.partner", "search",
+ Arrays.asList(Arrays.asList(Arrays.asList("id", "=", 78)))
+ )));
+
+.. code-block:: json
+
+ []
+
+.. _PostgreSQL: http://www.postgresql.org
+.. _XML-RPC: http://en.wikipedia.org/wiki/XML-RPC
_logger.warning('Access Denied by ACLs for operation: %s, uid: %s, model: %s', mode, uid, model_name)
msg = '%s %s' % (msg_heads[mode], msg_tail)
raise openerp.exceptions.AccessError(msg % msg_params)
- return r or False
+ return bool(r)
__cache_clearing_methods = []
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
@api.returns('self')
def search(self, cr, user, args, offset=0, limit=None, order=None, context=None, count=False):
- """ search(args[, offset=0][, limit=None][, order=None][, count=False])
+ """ search(args[, offset=0][, limit=None][, order=None])
Searches for records based on the ``args``
:ref:`search domain <reference/orm/domains>`.
:param int offset: number of results to ignore (default: none)
:param int limit: maximum number of records to return (default: all)
:param str order: sort string
- :param bool count: if ``True``, the call should return the number of
- records matching ``args`` rather than the records
- themselves.
:returns: at most ``limit`` records matching the search criteria
:raise AccessError: * if user tries to bypass access rules for read on the requested object.
if not partial:
raise
+ # update columns (fields may have changed), and column_infos
+ for name, field in self._fields.iteritems():
+ if field.store:
+ 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():
"""
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. """
_symbol_set = (_symbol_c, _symbol_f)
_symbol_get = None
_deprecated = False
- copy = True # whether the field is copied by BaseModel.copy()
+
+ copy = True # whether value is copied by BaseModel.copy()
+ string = None
+ help = ""
+ required = False
+ readonly = False
+ _domain = []
+ _context = {}
+ states = None
+ priority = 0
+ change_default = False
+ size = None
+ ondelete = None
+ translate = False
+ select = False
+ manual = False
+ write = False
+ read = False
+ selectable = True
+ group_operator = False
+ groups = False # CSV list of ext IDs of groups
+ deprecated = False # Optional deprecation warning
def __init__(self, string='unknown', required=False, readonly=False, domain=None, context=None, states=None, priority=0, change_default=False, size=None, ondelete=None, translate=False, select=False, manual=False, **args):
"""
It corresponds to the 'state' column in ir_model_fields.
"""
- if domain is None:
- domain = []
- if context is None:
- context = {}
- self.states = states or {}
- self.string = string
- self.readonly = readonly
- self.required = required
- self.size = size
- self.help = args.get('help', '')
- self.priority = priority
- self.change_default = change_default
- self.ondelete = ondelete.lower() if ondelete else 'set null'
- self.translate = translate
- self._domain = domain
- self._context = context
- self.write = False
- self.read = False
- self.select = select
- self.manual = manual
- self.selectable = True
- self.group_operator = args.get('group_operator', False)
- self.groups = False # CSV list of ext IDs of groups that can access this field
- self.deprecated = False # Optional deprecation warning
+ args0 = {
+ 'string': string,
+ 'required': required,
+ 'readonly': readonly,
+ '_domain': domain,
+ '_context': context,
+ 'states': states,
+ 'priority': priority,
+ 'change_default': change_default,
+ 'size': size,
+ 'ondelete': ondelete.lower() if ondelete else None,
+ 'translate': translate,
+ 'select': select,
+ 'manual': manual,
+ }
+ for key, val in args0.iteritems():
+ if val:
+ setattr(self, key, val)
+
self._args = args
- for a in args:
- setattr(self, a, args[a])
+ for key, val in args.iteritems():
+ setattr(self, key, val)
# prefetch only if self._classic_write, not self.groups, and not
# self.deprecated
base_items = [
('column', self), # field interfaces self
('copy', self.copy),
+ ]
+ truthy_items = filter(itemgetter(1), [
('index', self.select),
('manual', self.manual),
('string', self.string),
('groups', self.groups),
('change_default', self.change_default),
('deprecated', self.deprecated),
- ]
- truthy_items = filter(itemgetter(1), [
('size', self.size),
('ondelete', self.ondelete),
('translate', self.translate),
_symbol_f = lambda x: x or None
_symbol_set = (_symbol_c, _symbol_f)
+ ondelete = 'set null'
+
def __init__(self, obj, string='unknown', auto_join=False, **args):
_column.__init__(self, string=string, **args)
self._obj = obj
}
def dispatch(method, params):
- if method in ['login', 'about', 'timezone_get',
- 'version', 'authenticate']:
- pass
- elif method in ['set_loglevel']:
- passwd = params[0]
- params = params[1:]
- security.check_super(passwd)
- else:
+ if method not in ['login', 'about', 'timezone_get',
+ 'version', 'authenticate', 'set_loglevel']:
raise Exception("Method not found: %s" % method)
fn = globals()['exp_' + method]
@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', 'conditional']
+__all__ = ['synchronized', 'lazy_property', 'classproperty', 'conditional']
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:
image.save(background_stream, filetype)
return background_stream.getvalue().encode(encoding)
-def image_resize_and_sharpen(image, size, factor=2.0):
+def image_resize_and_sharpen(image, size, preserve_aspect_ratio=False, factor=2.0):
"""
Create a thumbnail by resizing while keeping ratio.
A sharpen filter is applied for a better looking result.
:param image: PIL.Image.Image()
:param size: 2-tuple(width, height)
+ :param preserve_aspect_ratio: boolean (default: False)
:param factor: Sharpen factor (default: 2.0)
"""
if image.mode != 'RGBA':
image = image.convert('RGBA')
image.thumbnail(size, Image.ANTIALIAS)
- size = image.size
+ if preserve_aspect_ratio:
+ size = image.size
sharpener = ImageEnhance.Sharpness(image)
resized_image = sharpener.enhance(factor)
# create a transparent image for background and paste the image on it