[MERGE] forward port of branch 8.0 up to 92c7874

This commit is contained in:
Christophe Simonis
2014-10-29 19:45:09 +01:00
55 changed files with 1848 additions and 411 deletions
+6 -1
View File
@@ -383,12 +383,17 @@ class account_bank_statement(osv.osv):
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):
@@ -809,7 +814,7 @@ class account_bank_statement_line(osv.osv):
'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'),
+13 -3
View File
@@ -318,16 +318,26 @@ class account_invoice(models.Model):
@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)
+23 -5
View File
@@ -47,6 +47,16 @@ class account_fiscal_position(osv.osv):
'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:
@@ -69,12 +79,13 @@ class account_fiscal_position(osv.osv):
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
@@ -115,10 +126,17 @@ class account_fiscal_position(osv.osv):
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
+2 -2
View File
@@ -16,8 +16,8 @@
<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">
@@ -33,10 +33,6 @@ class partner_balance(report_sxw.rml_parse, common_report_header):
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,
@@ -70,7 +66,20 @@ class partner_balance(report_sxw.rml_parse, common_report_header):
"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']
@@ -236,62 +245,6 @@ class partner_balance(report_sxw.rml_parse, common_report_header):
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':
@@ -392,7 +392,7 @@ class account_invoice(osv.osv):
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})
@@ -1,3 +1,5 @@
@charset "utf-8"
@mixin radius($radius: 5px)
-moz-border-radius: $radius
-webkit-border-radius: $radius
+1 -1
View File
@@ -58,4 +58,4 @@
// singleton
bus.bus = new bus.Bus();
return bus;
})();
})();
@@ -7,6 +7,7 @@
name="hr_timesheet_invoice.report_analyticprofit"
file="hr_timesheet_invoice.report_analyticprofit"
report_type="qweb-pdf"
menu="False"
string="Timesheet Profit"
/>
</data>
@@ -39,13 +39,15 @@ class account_analytic_profit(report_sxw.rml_parse):
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)
+149
View File
@@ -20,6 +20,7 @@
</p>
</div>
</div>
<h3>Actif</h3>
<table class="table table-condensed">
<thead>
<tr>
@@ -538,6 +539,154 @@
</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&#xE9;...]</td>
<td><span t-esc="bpvar1" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/></td>
</tr>
<tr>
<td>Primes d'&#xE9;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>&#xC9;carts de r&#xE9;&#xE9;valuation</td>
<td><span t-esc="bpvar3" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/></td>
</tr>
<tr>
<td>&#xC9;cart d'&#xE9;quivalence</td>
<td><span t-esc="bpvar4" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/></td>
</tr>
<tr>
<td><strong>R&#xC9;SERVES</strong></td>
<td></td>
</tr>
<tr>
<td>R&#xE9;serve l&#xE9;gale</td>
<td><span t-esc="bpvar5" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/></td>
</tr>
<tr>
<td>R&#xE9;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&#xE9;serves r&#xE9;glement&#xE9;es</td><td><span t-esc="bpvar7" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/></td></tr>
<tr>
<td>Autres r&#xE9;serves</td>
<td><span t-esc="bpvar8" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/></td>
</tr>
<tr>
<td>Report &#xE0; nouveau</td>
<td><span t-esc="bpvar9" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/></td>
</tr>
<tr>
<td><strong>R&#xC9;SULTAT DE L'EXERCICE [b&#xE9;n&#xE9;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&#xE9;glement&#xE9;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&#xE8;s des &#xE9;tablissements de cr&#xE9;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&#xE8;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&#xE7;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&#xE9;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&#xE9;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&#xE9;sorerie</td>
<td><span t-esc="bpvar24" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/></td>
</tr>
<tr>
<td>Produits constat&#xE9;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>&#xC9;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&#xC9;N&#xC9;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>&amp;nbsp;</td>
<td>&amp;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>
+15 -2
View File
@@ -931,9 +931,19 @@ class mrp_production(osv.osv):
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
@@ -952,13 +962,16 @@ class mrp_production(osv.osv):
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)
+6 -1
View File
@@ -315,7 +315,12 @@ class product_pricelist(osv.osv):
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:
+30 -1
View File
@@ -557,7 +557,7 @@ class product_template(osv.osv):
'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),
@@ -918,6 +918,7 @@ class product_product(osv.osv):
'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",
@@ -1110,6 +1111,34 @@ class product_product(osv.osv):
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"
+2 -1
View File
@@ -1,5 +1,6 @@
from . import test_uom
from . import test_uom, test_pricelist
fast_suite = [
test_uom,
test_pricelist
]
+70
View File
@@ -0,0 +1,70 @@
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)
@@ -43,6 +43,7 @@ class sale_order_line(osv.osv):
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')
@@ -58,9 +59,8 @@ class sale_order_line(osv.osv):
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
@@ -77,7 +77,7 @@ class sale_order_line(osv.osv):
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],
+121 -117
View File
@@ -1,125 +1,129 @@
<?xml version="1.0" encoding="utf-8"?>
<openerp>
<data>
<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>
<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>
<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">
<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>
</template>
<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>
<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>
<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">
<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>
<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>
@@ -1,67 +1,71 @@
<?xml version="1.0" encoding="utf-8"?>
<openerp>
<data>
<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>
<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>
<p t-field="o.notes"/>
<span>Regards,</span>
<span t-field="user.signature"/>
<div class="oe_structure"/>
</div>
</t>
</template>
<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"/>
<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>
<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"/>
<div class="oe_structure"/>
</div>
</t>
<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>
+2 -1
View File
@@ -101,7 +101,8 @@ class sale_order_line_make_invoice(osv.osv_memory):
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!'))
+5 -1
View File
@@ -36,7 +36,11 @@ class sale_order_line(osv.osv):
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)
+1 -1
View File
@@ -1267,7 +1267,7 @@ class stock_picking(osv.osv):
'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
+2 -2
View File
@@ -1068,11 +1068,11 @@ openerp.Session = openerp.Class.extend(openerp.PropertiesMixin, {
},
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.
@@ -832,7 +832,7 @@ openerp.web_calendar = function(instance) {
}
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,
+1 -1
View File
@@ -48,7 +48,7 @@ class DiagramView(openerp.http.Controller):
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]
})
+1 -1
View File
@@ -21,7 +21,7 @@ class TableExporter(http.Controller):
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;")
+1 -1
View File
@@ -616,7 +616,7 @@ class website(osv.osv):
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)
+34 -14
View File
@@ -131,12 +131,13 @@ OTHER DEALINGS IN THE SOFTWARE.
// 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>'
@@ -147,6 +148,7 @@ OTHER DEALINGS IN THE SOFTWARE.
+ '<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");
@@ -165,12 +167,20 @@ OTHER DEALINGS IN THE SOFTWARE.
_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] : "";
@@ -182,7 +192,10 @@ OTHER DEALINGS IN THE SOFTWARE.
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();
@@ -205,7 +218,6 @@ OTHER DEALINGS IN THE SOFTWARE.
}
transfo.settings.css = window.getComputedStyle($this[0], null);
transfo.settings.pos = $this.offset();
transfo.settings.rotationStep = 5;
transfo.settings.hide = false;
@@ -226,7 +238,7 @@ OTHER DEALINGS IN THE SOFTWARE.
}
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) {
@@ -339,6 +351,7 @@ OTHER DEALINGS IN THE SOFTWARE.
settings.scaley = Math.round(settings.scaley*100)/100;
_targetCss($this, transfo);
_stop_animation($this[0]);
return false;
}
@@ -394,15 +407,22 @@ OTHER DEALINGS IN THE SOFTWARE.
_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);
@@ -412,10 +432,10 @@ OTHER DEALINGS IN THE SOFTWARE.
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();
}
}
@@ -1472,10 +1472,11 @@
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;");
@@ -88,6 +88,9 @@ class BlogPost(osv.Model):
'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'),
}
@@ -131,11 +131,7 @@
</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>
+5 -6
View File
@@ -369,7 +369,6 @@ class website_sale(http.Controller):
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)
@@ -434,10 +433,10 @@ class website_sale(http.Controller):
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
@@ -458,7 +457,7 @@ class website_sale(http.Controller):
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'):
@@ -341,6 +341,11 @@ label.css_attribute_color.css_not_available {
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;
@@ -287,6 +287,10 @@ label.css_attribute_color.css_not_available
.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
@@ -7,10 +7,12 @@ $('.oe_website_sale').each(function () {
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")] || "" );
@@ -95,7 +97,7 @@ $('.oe_website_sale').each(function () {
$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");
});
@@ -129,7 +131,7 @@ $('.oe_website_sale').each(function () {
}
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);
+4 -4
View File
@@ -823,7 +823,7 @@
<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">
@@ -862,7 +862,7 @@
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>
@@ -887,12 +887,12 @@
<input type="text" name="shipping_city" class="form-control" t-att-value="checkout.get('shipping_city', '')" t-att-readonly=" 'readonly' if shipping_id &gt;= 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 &gt;= 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 &gt;= 0 else ''">
<select name="shipping_country_id" class="form-control" t-att-disabled=" 'disabled' if shipping_id &gt;= 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>
@@ -53,7 +53,7 @@ $(document).ready(function () {
$(".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");
}
});
@@ -65,5 +65,14 @@ $(document).ready(function () {
});
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');
}
});
});
@@ -131,6 +131,7 @@
<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>
+5
View File
@@ -25,3 +25,8 @@ class Exercise(admonitions.BaseAdmonition):
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)
+12 -1
View File
@@ -21,7 +21,7 @@
{%- endblock -%}
{%- block content -%}
<div class="document-super">
<div class="document-super {% if meta is defined %}{{ meta.classes }}{% endif %}">
{{ super() }}
</div>
{%- endblock -%}
@@ -113,4 +113,15 @@
</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 %}
+70
View File
@@ -25,6 +25,76 @@ $(function () {
});
}, 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',
+105 -22
View File
@@ -6174,7 +6174,11 @@ button.close {
display: none !important;
}
}
* {
box-sizing: border-box;
}
body {
overflow: auto;
position: relative;
}
.document-super {
@@ -6643,20 +6647,11 @@ div.section > h2 {
*
* 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
@@ -6665,6 +6660,11 @@ div.section > h2 {
position: relative;
display: none;
}
@media (min-width: 768px) {
.zero-clipboard {
display: block;
}
}
.btn-clipboard {
position: absolute;
top: 0;
@@ -6684,11 +6684,6 @@ div.section > h2 {
background-color: #a24689;
border-color: #a24689;
}
@media (min-width: 768px) {
.zero-clipboard {
display: block;
}
}
img.align-center {
display: block;
margin: 0 auto;
@@ -6710,10 +6705,98 @@ td.field-body > ul {
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;
}
+113 -26
View File
@@ -17,7 +17,11 @@
// indent level for various items list e.g. dl, fields lists, ...
@item-indent: 30px;
* {
box-sizing: border-box;
}
body {
overflow: auto;
position: relative;
}
@@ -461,21 +465,13 @@ div.section > h2 {
*
* 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;
}
/*
@@ -485,6 +481,9 @@ div.section > h2 {
.zero-clipboard {
position: relative;
display: none;
@media (min-width: @screen-sm-min) {
display: block;
}
}
.btn-clipboard {
position: absolute;
@@ -506,12 +505,6 @@ div.section > h2 {
border-color: @brand-primary;
}
@media (min-width: 768px) {
.zero-clipboard {
display: block;
}
}
// rST styles
img.align-center {
display: block;
@@ -546,13 +539,107 @@ td.field-body {
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;
}
}
+9
View File
@@ -187,6 +187,9 @@ def setup(app):
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
@@ -212,6 +215,12 @@ def versionize(app, pagename, templatename, context, doctree):
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,
+1
View File
@@ -5,3 +5,4 @@ Module Objects
.. toctree::
:titlesonly:
modules/api_integration
+816
View File
@@ -0,0 +1,816 @@
: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
+1 -1
View File
@@ -782,7 +782,7 @@ class ir_model_access(osv.osv):
_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 = []
+2 -1
View File
@@ -319,7 +319,8 @@ class Field(object):
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
+7 -4
View File
@@ -1654,7 +1654,7 @@ class BaseModel(object):
@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>`.
@@ -1664,9 +1664,6 @@ class BaseModel(object):
: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.
@@ -2996,6 +2993,12 @@ class BaseModel(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():
+21 -5
View File
@@ -23,13 +23,14 @@
"""
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__)
@@ -259,12 +260,27 @@ class RegistryManager(object):
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. """
+47 -29
View File
@@ -83,7 +83,28 @@ class _column(object):
_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):
"""
@@ -92,33 +113,28 @@ class _column(object):
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
@@ -150,6 +166,8 @@ class _column(object):
base_items = [
('column', self), # field interfaces self
('copy', self.copy),
]
truthy_items = filter(itemgetter(1), [
('index', self.select),
('manual', self.manual),
('string', self.string),
@@ -160,8 +178,6 @@ class _column(object):
('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),
@@ -620,6 +636,8 @@ class many2one(_column):
_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
+2 -8
View File
@@ -18,14 +18,8 @@ RPC_VERSION_1 = {
}
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]
+7 -5
View File
@@ -477,8 +477,6 @@ class ConnectionPool(object):
@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:
@@ -503,7 +501,7 @@ class ConnectionPool(object):
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
@@ -546,11 +544,15 @@ class ConnectionPool(object):
@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):
+9 -1
View File
@@ -20,7 +20,7 @@
#
##############################################################################
__all__ = ['synchronized', 'lazy_property', 'conditional']
__all__ = ['synchronized', 'lazy_property', 'classproperty', 'conditional']
from functools import wraps
from inspect import getsourcefile
@@ -116,4 +116,12 @@ def compose(a, b):
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:
+4 -2
View File
@@ -96,19 +96,21 @@ def image_resize_image(base64_source, size=(1024, 1024), encoding='base64', file
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