[MERGE] Forward-port saas-15 up to 878fbc75ff

This commit is contained in:
Olivier Dony
2017-05-16 17:09:45 +02:00
51 changed files with 394 additions and 163 deletions
@@ -426,7 +426,7 @@ class AccountBankStatementLine(models.Model):
payment_to_unreconcile |= line.payment_id
if st_line.move_name and line.payment_id.payment_reference == st_line.move_name:
#there can be several moves linked to a statement line but maximum one created by the line itself
aml_to_cancel |= st_line.journal_entry_ids
aml_to_cancel |= line
payment_to_cancel |= line.payment_id
aml_to_unbind = aml_to_unbind - aml_to_cancel
@@ -887,6 +887,8 @@ class AccountBankStatementLine(models.Model):
if aml_dict.get('tax_ids') and isinstance(aml_dict['tax_ids'][0], pycompat.integer_types):
# Transform the value in the format required for One2many and Many2many fields
aml_dict['tax_ids'] = map(lambda id: (4, id, None), aml_dict['tax_ids'])
if any(line.journal_entry_ids for line in self):
raise UserError(_('A selected statement line was already reconciled with an account move.'))
# Fully reconciled moves are just linked to the bank statement
total = self.amount
+7 -2
View File
@@ -79,8 +79,7 @@ Best Regards,''')
account.write({'code': account.code.rstrip('0').ljust(digits, '0')})
@api.multi
def write(self, values):
#restrict the closing of FY if there are still unposted entries
def _validate_fiscalyear_lock(self, values):
if values.get('fiscalyear_lock_date'):
nb_draft_entries = self.env['account.move'].search([
('company_id', 'in', [c.id for c in self]),
@@ -88,6 +87,12 @@ Best Regards,''')
('date', '<=', values['fiscalyear_lock_date'])])
if nb_draft_entries:
raise ValidationError(_('There are still unposted entries in the period you want to lock. You should either post or delete them.'))
@api.multi
def write(self, values):
#restrict the closing of FY if there are still unposted entries
self._validate_fiscalyear_lock(values)
# Reflect the change on accounts
for company in self:
digits = values.get('accounts_code_digits') or company.accounts_code_digits
@@ -108,7 +108,7 @@ class AccountInvoiceReport(models.Model):
1 AS nbr,
ai.type, ai.state, pt.categ_id, ai.date_due, ai.account_id, ail.account_id AS account_line_id,
ai.partner_bank_id,
SUM ((invoice_type.sign * ail.quantity) / (u.factor * u2.factor)) AS product_qty,
SUM ((invoice_type.sign * ail.quantity) / u.factor * u2.factor) AS product_qty,
SUM(ail.price_subtotal_signed) AS price_total,
SUM(ABS(ail.price_subtotal_signed)) / CASE
WHEN SUM(ail.quantity / u.factor * u2.factor) <> 0::numeric
+3
View File
@@ -57,6 +57,9 @@ class AuthSignupHome(Home):
else:
login = qcontext.get('login')
assert login, "No login provided."
_logger.info(
"Password reset attempt for <%s> by user <%s> from %s",
login, request.env.user.login, request.httprequest.remote_addr)
request.env['res.users'].sudo().reset_password(login)
qcontext['message'] = _("An email has been sent with credentials to reset your password")
except SignupError:
+3
View File
@@ -1,6 +1,7 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import logging
from ast import literal_eval
@@ -11,6 +12,7 @@ from odoo.tools.misc import ustr
from odoo.addons.base.ir.ir_mail_server import MailDeliveryException
from odoo.addons.auth_signup.models.res_partner import SignupError, now
_logger = logging.getLogger(__name__)
class ResUsers(models.Model):
_inherit = 'res.users'
@@ -134,6 +136,7 @@ class ResUsers(models.Model):
if not user.email:
raise UserError(_("Cannot send email: user %s has no email address.") % user.name)
template.with_context(lang=user.lang).send_mail(user.id, force_send=True, raise_exception=True)
_logger.info("Password reset email sent for user <%s> to <%s>", user.login, user.email)
@api.model
def create(self, values):
@@ -15,6 +15,13 @@
attrs="{'invisible':[('menu_ir_values_id','=',False)]}"
help="Remove the contextual action related to this server action"/>
</xpath>
<div class="oe_title" position="before">
<div class="oe_button_box" name="button_box">
<button name="toggle_active" type="object" class="oe_stat_button" icon="fa-archive">
<field name="active" widget="boolean_button"/>
</button>
</div>
</div>
<xpath expr="//group[@name='action_wrapper']" position="inside">
<group>
<field name="trigger"/>
@@ -8,13 +8,10 @@
<field name="arch" type="xml">
<xpath expr="//div[@id='msg_module_google_spreadsheet']" position="replace">
<div class="content-group" attrs="{'invisible': [('module_google_spreadsheet','=',False)]}">
<div class="row mt16">
<label for="google_drive_authorization_code" class="col-md-3 o_light_label"/>
<field name="google_drive_authorization_code"/>
</div>
<a type="button" class="btn-link" target="_blank"><i class="fa fa-fw fa-arrow-right"/>
<field name="google_drive_uri_copy" widget="url" text="Generate Code"/>
</a>
<div class="content-group mt16">
Please use the settings of Google Drive
on the left or above.
</div>
</div>
</xpath>
</field>
@@ -166,7 +166,7 @@ class ImLivechatChannel(models.Model):
operator_partner_id = user.partner_id.id
# partner to add to the mail.channel
channel_partner_to_add = [(4, operator_partner_id)]
if self.env.uid: # if the user if logged (portal user), he can be identify
if self.env.user and self.env.user.active: # valid session user (not public)
channel_partner_to_add.append((4, self.env.user.partner_id.id))
# create the session, and add the link with the given channel
mail_channel = self.env["mail.channel"].with_context(mail_create_nosubscribe=False).sudo().create({
@@ -48,7 +48,7 @@
</td>
<td>
<span t-esc="o.employee_id.id or '_'"/><br/>
<span t-esc="o.employee_id.ssnid or '_'"/><br/>
<span t-esc="o.employee_id.identification_id or '_'"/><br/>
<span t-esc="o.contract_id.job_id.name or '_'"/><br/>
<span t-esc="o.contract_id.qualif or '_'"/><br/>
<span t-esc="o.contract_id.niveau or '_'"/><br/>
+1 -1
View File
@@ -507,7 +507,7 @@ class MassMailing(models.Model):
return super(MassMailing, self).read_group(domain, fields, groupby, offset=offset, limit=limit, orderby=orderby)
def update_opt_out(self, email, res_ids, value):
model = self.env[self.mailing_model_real]
model = self.env[self.mailing_model_real].with_context(active_test=False)
if 'opt_out' in model._fields:
email_fname = 'email_from'
if 'email' in model._fields:
@@ -273,9 +273,13 @@ snippets_editor.Class.include({
function editable_area_is_empty($layout) {
$layout = $layout || $editable_area.find(".o_layout");
var $mail_wrapper = $layout.children(".o_mail_wrapper");
var $mail_wrapper_content = $mail_wrapper.find('.o_mail_wrapper_td');
if (!$mail_wrapper_content.length) { // compatibility
$mail_wrapper_content = $mail_wrapper;
}
return (
$editable_area.html().trim() === ""
|| ($layout.length > 0 && ($layout.html().trim() === "" || $mail_wrapper.length > 0 && $mail_wrapper.html().trim() === ""))
|| ($layout.length > 0 && ($layout.html().trim() === "" || $mail_wrapper_content.length > 0 && $mail_wrapper_content.html().trim() === ""))
);
}
@@ -325,7 +329,15 @@ snippets_editor.Class.include({
switch_images(theme_params, $editable_area);
var $old_layout = $editable_area.find(".o_layout");
var $new_wrapper = $("<div/>", {"class": "o_mail_wrapper oe_structure"});
// This wrapper structure is the only way to have a responsive and
// centered fixed-width content column on all mail clients
var $new_wrapper = $('<table/>', {class: 'o_mail_wrapper'});
var $new_wrapper_content = $("<td/>", {class: 'o_mail_no_resize o_mail_wrapper_td oe_structure'});
$new_wrapper.append($('<tr/>').append(
$("<td/>", {class: 'o_mail_no_resize'}),
$new_wrapper_content,
$("<td/>", {class: 'o_mail_no_resize'})
));
var $new_layout = $("<div/>", {"class": "o_layout " + theme_params.className}).append($new_wrapper);
var $contents;
@@ -338,11 +350,11 @@ snippets_editor.Class.include({
}
$editable_area.empty().append($new_layout);
$new_wrapper.append($contents);
$new_wrapper_content.append($contents);
$old_layout.remove();
if (first_choice) {
self.add_default_snippet_text_classes($new_wrapper);
self.add_default_snippet_text_classes($new_wrapper_content);
}
self.show_blocks();
}
@@ -49,27 +49,36 @@
// ===== Layout =====
.o_layout {
font-family: @o_mm_def_font;
color: lighten(@o_mm_def_text_color, 20%);
overflow: hidden;
width: 100%;
width: 94%;
min-height: 100%;
background-color: @o_mm_def_color_alpha;
padding: 24px 0;
padding: 24px 3%;
// Forces on <p/> elements as several mail clients does not correctly
// inherit font properties
&, p {
font-family: @o_mm_def_font;
color: lighten(@o_mm_def_text_color, 20%);
}
> .o_mail_wrapper {
width: 100%;
max-width: @o_mm_def_body_width;
margin: auto;
border: 1px solid darken(@o_mm_def_color_alpha, 2%);
background-color: @o_mm_def_color_beta;
border-collapse: separate; // Allow the first and last td to have the
// same width
.o_mail_wrapper_td {
width: @o_mm_def_body_width;
border: 1px solid darken(@o_mm_def_color_alpha, 2%);
background-color: @o_mm_def_color_beta;
}
}
}
// ===== Snippets (general) =====
.o_mail_snippet_general {
width: 100%;
max-width: @o_mm_def_body_width;
max-width: @o_mm_def_body_width; // should not be necessary thanks to mail wrapper
margin: 0 auto;
h1, h2, h3, h4, h5, h6, .h1, .h2, .h3, .h4, .h5, .h6 {
@@ -157,7 +166,11 @@
}
.o_mail_block_two_cols, .o_mail_block_three_cols {
font-size: small;
// Forces on <p/> elements as several mail clients does not correctly
// inherit font properties
&, p {
font-size: small;
}
.o_mail_col_container {
padding: 10px 20px;
@@ -169,7 +182,11 @@
}
.o_mail_block_image_text, .o_mail_block_text_image {
font-size: small;
// Forces on <p/> elements as several mail clients does not correctly
// inherit font properties
&, p {
font-size: small;
}
}
.o_mail_block_event {
+3
View File
@@ -499,11 +499,14 @@ class MrpProduction(models.Model):
@api.multi
def post_inventory(self):
for order in self:
moves_not_to_do = order.move_raw_ids.filtered(lambda x: x.state == 'done')
moves_to_do = order.move_raw_ids.filtered(lambda x: x.state not in ('done', 'cancel'))
moves_to_do.action_done()
moves_to_do = order.move_raw_ids.filtered(lambda x: x.state == 'done') - moves_not_to_do
order._cal_price(moves_to_do)
moves_to_finish = order.move_finished_ids.filtered(lambda x: x.state not in ('done','cancel'))
moves_to_finish.action_done()
for move in moves_to_finish:
#Group quants by lots
lot_quants = {}
+6 -4
View File
@@ -507,7 +507,7 @@ class PaymentTransaction(models.Model):
tx.write({'reference': str(tx.id)})
# Generate callback hash if it is configured on the tx; avoid generating unnecessary stuff
if tx.callback_model_id and tx.callback_res_id and tx.callback_method:
if tx.callback_model_id and tx.callback_res_id and tx.sudo().callback_method:
tx.write({'callback_hash': tx._generate_callback_hash()})
return tx
@@ -547,7 +547,9 @@ class PaymentTransaction(models.Model):
def _generate_callback_hash(self):
self.ensure_one()
secret = self.env['ir.config_parameter'].sudo().get_param('database.secret')
token = '%s%s%s' % (self.callback_model_id.model, self.callback_res_id, self.callback_method)
token = '%s%s%s' % (self.callback_model_id.model,
self.callback_res_id,
self.sudo().callback_method)
return hmac.new(str(secret), token, hashlib.sha256).hexdigest()
# --------------------------------------------------
@@ -635,7 +637,7 @@ class PaymentTransaction(models.Model):
@api.multi
def execute_callback(self):
res = None
for transaction in self.filtered(lambda tx: tx.callback_model_id and tx.callback_res_id and tx.callback_method):
for transaction in self.filtered(lambda tx: tx.callback_model_id and tx.callback_res_id and tx.sudo().callback_method):
valid_token = transaction._generate_callback_hash()
if not consteq(ustr(valid_token), transaction.callback_hash):
_logger.warning("Invalid callback signature for transaction %d" % (transaction.id))
@@ -643,7 +645,7 @@ class PaymentTransaction(models.Model):
record = self.env[transaction.callback_model_id.model].browse(transaction.callback_res_id).exists()
if record:
res = getattr(record, transaction.callback_method)(transaction)
res = getattr(record, transaction.sudo().callback_method)(transaction)
else:
_logger.warning("Did not found record %s.%s for callback of transaction %d" % (transaction.callback_model_id.model, transaction.callback_res_id, transaction.id))
return res
+1 -1
View File
@@ -677,7 +677,7 @@ class PosOrder(models.Model):
qty = 0
qty_done = 0
pack_lots = []
pos_pack_lots = PosPackOperationLot.search([('order_id', '=', order.id), ('product_id', '=', pack_operation.product_id.id)])
pos_pack_lots = PosPackOperationLot.search([('order_id', '=', order.id), ('product_id', '=', pack_operation.product_id.id)])
pack_lot_names = [pos_pack.lot_name for pos_pack in pos_pack_lots]
if pack_lot_names and lots_necessary:
+12 -12
View File
@@ -753,7 +753,7 @@
<t t-if='simple'>
<line>
<left><t t-esc='line.product_name_wrapped[0]' /></left>
<right><value><t t-esc='line.price_display' /></value></right>
<right><value t-att-value-decimals='pos.currency.decimals'>><t t-esc='line.price_display' /></value></right>
</line>
<t t-call="XmlReceiptWrappedProductNameLines"/>
</t>
@@ -765,19 +765,19 @@
</t>
<line indent='1'>
<left>
<value value-decimals='3' value-autoint='on'>
<value t-att-value-decimals='pos.dp["Product Unit of Measure"]' value-autoint='on'>
<t t-esc='line.quantity' />
</value>
<t t-if='line.unit_name !== "Unit(s)"'>
<t t-esc='line.unit_name' />
</t>
x
<value value-decimals='2'>
<value t-att-value-decimals='pos.dp["Product Price"]'>
<t t-esc='line.price' />
</value>
</left>
<right>
<value><t t-esc='line.price_display' /></value>
<value t-att-value-decimals='pos.currency.decimals'><t t-esc='line.price_display' /></value>
</right>
</line>
</t>
@@ -789,11 +789,11 @@
<t t-set='taxincluded' t-value='Math.abs(receipt.subtotal - receipt.total_with_tax) &lt;= 0.000001' />
<t t-if='!taxincluded'>
<line><right>--------</right></line>
<line><left>Subtotal</left><right> <value><t t-esc="receipt.subtotal" /></value></right></line>
<line><left>Subtotal</left><right><value t-att-value-decimals='pos.currency.decimals'><t t-esc="receipt.subtotal" /></value></right></line>
<t t-foreach='receipt.tax_details' t-as='tax'>
<line>
<left><t t-esc='tax.name' /></left>
<right><value><t t-esc='tax.amount' /></value></right>
<right><value t-att-value-decimals='pos.currency.decimals'><t t-esc='tax.amount' /></value></right>
</line>
</t>
</t>
@@ -803,7 +803,7 @@
<line><right>--------</right></line>
<line class='total' size='double-height'>
<left><pre> TOTAL</pre></left>
<right><value><t t-esc='receipt.total_with_tax' /></value></right>
<right><value t-att-value-decimals='pos.currency.decimals'><t t-esc='receipt.total_with_tax' /></value></right>
</line>
<br/><br/>
@@ -812,14 +812,14 @@
<t t-foreach='paymentlines' t-as='line'>
<line>
<left><t t-esc='line.name' /></left>
<right><value><t t-esc='line.get_amount()'/></value></right>
<right><value t-att-value-decimals='pos.currency.decimals'><t t-esc='line.get_amount()'/></value></right>
</line>
</t>
<br/>
<line size='double-height'>
<left><pre> CHANGE</pre></left>
<right><value><t t-esc='receipt.change' /></value></right>
<right><value t-att-value-decimals='pos.currency.decimals'><t t-esc='receipt.change' /></value></right>
</line>
<br/>
@@ -828,19 +828,19 @@
<t t-if='receipt.total_discount'>
<line>
<left>Discounts</left>
<right><value><t t-esc='receipt.total_discount'/></value></right>
<right><value t-att-value-decimals='pos.currency.decimals'><t t-esc='receipt.total_discount'/></value></right>
</line>
</t>
<t t-if='taxincluded'>
<t t-foreach='receipt.tax_details' t-as='tax'>
<line>
<left><t t-esc='tax.name' /></left>
<right><value><t t-esc='tax.amount' /></value></right>
<right><value t-att-value-decimals='pos.currency.decimals'><t t-esc='tax.amount' /></value></right>
</line>
</t>
<line>
<left>Total Taxes</left>
<right><value><t t-esc='receipt.total_tax'/></value></right>
<right><value t-att-value-decimals='pos.currency.decimals'><t t-esc='receipt.total_tax'/></value></right>
</line>
</t>
@@ -17,7 +17,7 @@ class MercuryTransaction(models.Model):
_name = 'pos_mercury.mercury_transaction'
def _get_pos_session(self):
pos_session = self.env['pos.session'].search([('state', '=', 'opened'), ('user_id', '=', self.env.uid)])
pos_session = self.env['pos.session'].search([('state', '=', 'opened'), ('user_id', '=', self.env.uid)], limit=1)
if not pos_session:
raise UserError("No POS session")
@@ -34,6 +34,7 @@ class MercuryTransaction(models.Model):
raise UserError("No Mercury configuration associated with the journal.")
def _setup_request(self, data):
# todo: in master make the client include the pos.session id and use that
pos_session = self._get_pos_session()
config = pos_session.config_id
@@ -979,4 +979,11 @@ screens.define_action_button({
},
});
return {
TableGuestsButton: TableGuestsButton,
TransferOrderButton:TransferOrderButton,
TableWidget: TableWidget,
FloorScreenWidget: FloorScreenWidget,
};
});
+3 -2
View File
@@ -18,12 +18,13 @@ class ResCompany(models.Model):
'currency_id': new_company.currency_id.id,
})
field = self.env['ir.model.fields']._get('res.partner', 'property_product_pricelist')
self.env['ir.property'].create({
product_property = self.env['ir.property'].create({
'name': 'property_product_pricelist',
'company_id': new_company.id,
'value_reference': 'product.pricelist,%s' % pricelist.id,
'fields_id': field.id
})
# multi-company security rules prevents access
product_property.sudo().write({'company_id': new_company.id})
return new_company
@api.multi
+16 -12
View File
@@ -576,19 +576,23 @@ class Report(models.Model):
"""
writer = PdfFileWriter()
streams = [] # We have to close the streams *after* PdfFilWriter's call to write()
for document in documents:
pdfreport = open(document, 'rb')
streams.append(pdfreport)
reader = PdfFileReader(pdfreport)
for page in range(0, reader.getNumPages()):
writer.addPage(reader.getPage(page))
try:
for document in documents:
pdfreport = open(document, 'rb')
streams.append(pdfreport)
reader = PdfFileReader(pdfreport)
for page in range(0, reader.getNumPages()):
writer.addPage(reader.getPage(page))
merged_file_fd, merged_file_path = tempfile.mkstemp(suffix='.html', prefix='report.merged.tmp.')
with closing(os.fdopen(merged_file_fd, 'w')) as merged_file:
writer.write(merged_file)
for stream in streams:
stream.close()
merged_file_fd, merged_file_path = tempfile.mkstemp(suffix='.pdf', prefix='report.merged.tmp.')
with closing(os.fdopen(merged_file_fd, 'w')) as merged_file:
writer.write(merged_file)
finally:
for stream in streams:
try:
stream.close()
except Exception:
pass
return merged_file_path
-12
View File
@@ -12,17 +12,5 @@
<record id="report_sale_order" model="ir.actions.report.xml">
<field name="print_report_name">(object.state in ('draft', 'sent') and 'Quotation'+'-'+(object.name) or 'Order'+'-'+(object.name))</field>
</record>
<report
string="Pro-Forma"
id="report_proforma_quotation"
model="sale.order"
report_type="qweb-pdf"
name="sale.report_saleproforma"
file="sale.report_saleorder"
/>
<record id="report_proforma_quotation" model="ir.actions.report.xml">
<field name="print_report_name">'Pro-Forma'+'-'+(object.name)</field>
</record>
</data>
</odoo>
+2 -2
View File
@@ -29,11 +29,11 @@
</div>
<h2>
<t t-if="not proforma">
<t t-if="not env.context.get('proforma', False)">
<span t-if="doc.state not in ['draft','sent']">Order # </span>
<span t-if="doc.state in ['draft','sent']">Quotation # </span>
</t>
<t t-if="proforma">
<t t-if="env.context.get('proforma', False)">
<span>Pro-Forma # </span>
</t>
<span t-field="doc.name"/>
-1
View File
@@ -4,7 +4,6 @@
<record id="sale.group_sale_layout" model="res.groups">
<field name="name">Personalize sales order and invoice report</field>
<field name="category_id" ref="base.module_category_hidden"/>
<field name="implied_ids" eval="[(4, ref('base.group_user'))]"/>
</record>
<record id="group_delivery_invoice_address" model="res.groups">
@@ -82,10 +82,11 @@ class SaleAdvancePaymentInv(models.TransientModel):
else:
amount = self.amount
name = _('Down Payment')
if order.fiscal_position_id and self.product_id.taxes_id:
tax_ids = order.fiscal_position_id.map_tax(self.product_id.taxes_id).ids
taxes = self.product_id.taxes_id.filtered(lambda r: not order.company_id or r.company_id == order.company_id)
if order.fiscal_position_id and taxes:
tax_ids = order.fiscal_position_id.map_tax(taxes).ids
else:
tax_ids = self.product_id.taxes_id.ids
tax_ids = taxes.ids
invoice = inv_obj.create({
'name': order.client_order_ref or order.name,
@@ -145,10 +146,11 @@ class SaleAdvancePaymentInv(models.TransientModel):
raise UserError(_('The product used to invoice a down payment should have an invoice policy set to "Ordered quantities". Please update your deposit product to be able to create a deposit invoice.'))
if self.product_id.type != 'service':
raise UserError(_("The product used to invoice a down payment should be of type 'Service'. Please use another product or update this product."))
if order.fiscal_position_id and self.product_id.taxes_id:
tax_ids = order.fiscal_position_id.map_tax(self.product_id.taxes_id).ids
taxes = self.product_id.taxes_id.filtered(lambda r: not order.company_id or r.company_id == order.company_id)
if order.fiscal_position_id and taxes:
tax_ids = order.fiscal_position_id.map_tax(taxes).ids
else:
tax_ids = self.product_id.taxes_id.ids
tax_ids = taxes.ids
so_line = sale_line_obj.create({
'name': _('Advance: %s') % (time.strftime('%m %Y'),),
'price_unit': amount,
+24
View File
@@ -42,6 +42,30 @@ class SaleOrderLine(models.Model):
return 0.0
return super(SaleOrderLine, self)._get_delivered_qty()
@api.multi
def _get_bom_component_qty(self, bom):
bom_quantity = self.product_uom._compute_quantity(self.product_uom_qty, bom.product_uom_id)
boms, lines = bom.explode(self.product_id, bom_quantity)
components = {}
for line, line_data in lines:
product = line.product_id.id
uom = line.product_uom_id
qty = line.product_qty
if components.get(product, False):
if uom.id != components[product]['uom']:
from_uom = uom
to_uom = self.env['product.uom'].browse(components[product]['uom'])
qty = from_uom._compute_quantity(qty, to_uom_id=to_uom)
components[product]['qty'] += qty
else:
# To be in the uom reference of the product
to_uom = self.env['product.product'].browse(product).uom_id
if uom.id != to_uom.id:
from_uom = uom
qty = from_uom._compute_quantity(qty, to_uom_id=to_uom)
components[product] = {'qty': qty, 'uom': to_uom.id}
return components
class AccountInvoiceLine(models.Model):
# TDE FIXME: what is this code ??
+1 -1
View File
@@ -71,7 +71,7 @@ class ProcurementOrder(models.Model):
'partner_id': self.sale_line_id.order_id.partner_id.id or self.partner_dest_id.id,
'user_id': self.env.uid,
'procurement_id': self.id,
'description': self.name + '\n',
'description': self.name + '<br/>',
'project_id': project.id,
'company_id': self.company_id.id,
}
+6 -3
View File
@@ -273,6 +273,11 @@ class ProcurementOrder(models.Model):
def _procurement_from_orderpoint_post_process(self, orderpoint_ids):
return True
def _get_orderpoint_domain(self, company_id=False):
domain = [('company_id', '=', company_id)] if company_id else []
domain += [('product_id.active', '=', True)]
return domain
@api.model
def _procure_orderpoint_confirm(self, use_new_cursor=False, company_id=False):
""" Create procurements based on orderpoints.
@@ -282,9 +287,7 @@ class ProcurementOrder(models.Model):
"""
OrderPoint = self.env['stock.warehouse.orderpoint']
domain = [('company_id', '=', company_id)] if company_id else []
domain += [('product_id.active', '=', True)]
domain = self._get_orderpoint_domain(company_id=company_id)
orderpoints_noprefetch = OrderPoint.with_context(prefetch_fields=False).search(domain,
order=self._procurement_from_orderpoint_get_order()).ids
while orderpoints_noprefetch:
+5 -2
View File
@@ -23,14 +23,17 @@ class Company(models.Model):
location = self.env['stock.location'].create({
'name': _('%s: Transit Location') % self.name,
'usage': 'transit',
'company_id': self.id,
'location_id': parent_location and parent_location.id or False,
})
location.sudo().write({'company_id': self.id})
self.write({'internal_transit_location_id': location.id})
@api.model
def create(self, vals):
company = super(Company, self).create(vals)
self.env['stock.warehouse'].create({'name': company.name, 'code': company.name[:5], 'company_id': company.id})
# multi-company rules prevents creating warehouse and sub-locations
self.env['stock.warehouse'].check_access_rights('create')
self.env['stock.warehouse'].sudo().create({'name': company.name, 'code': company.name[:5], 'company_id': company.id})
company.create_transit_location()
return company
+4 -1
View File
@@ -495,9 +495,12 @@ class StockMove(models.Model):
self._push_apply()
return self
def _set_default_price_moves(self):
return self.filtered(lambda move: not move.price_unit)
def set_default_price_unit_from_product(self):
""" Set price to move, important in inter-company moves or receipts with only one partner """
for move in self.filtered(lambda move: not move.price_unit):
for move in self._set_default_price_moves():
move.write({'price_unit': move.product_id.standard_price})
attribute_price = set_default_price_unit_from_product
+1 -1
View File
@@ -226,7 +226,7 @@
<h1 class="hidden-xs">
<field name="name" attrs="{'invisible': [('name','=','/')]}" readonly="1"/>
</h1>
<group class="hidden-xs">
<group>
<group>
<field name="partner_id" attrs="{'invisible': [('picking_type_code', '=', 'internal')]}"/>
<field name="location_id" groups="stock.group_stock_multi_locations" attrs="{'invisible': [('picking_type_code', '=', 'outgoing')]}"/>
+6
View File
@@ -179,6 +179,12 @@ class StockMove(models.Model):
to_refund = fields.Boolean(string="To Refund (update SO/PO)",
help='Trigger a decrease of the delivered/received quantity in the associated Sale Order/Purchase Order')
def _set_default_price_moves(self):
# When the cost method is in real or average price, the price can be set to 0.0 on the PO
# So the price doesn't have to be updated
moves = super(StockMove, self)._set_default_price_moves()
return moves.filtered(lambda m: m.product_id.cost_method not in ('real', 'average'))
@api.multi
def action_done(self):
self.product_price_update_before_done()
@@ -168,6 +168,7 @@ var DebugManager = Widget.extend({
res_model: 'ir.ui.view',
title: _t('Select a view'),
disable_multiple_selection: true,
domain: [['type', '!=', 'qweb'], ['type', '!=', 'search']],
on_selected: function (element_ids) {
self._rpc({
model: 'ir.ui.view',
+23 -1
View File
@@ -1,6 +1,28 @@
.o_tooltip_parent {
position: relative!important;
// Tooltips are placed in the <body/> element when opened because this is
// the only way to position them above everything else thanks to a 1051
// z-index. However, for scrolling performance, the tooltip is placed in its
// ideal location (see Tip._get_ideal_location). When in this location, the
// tooltip were sometimes overlapping unwanted elements (e.g. chat windows).
//
// The z-index of the ideal locations cannot be determined safely as they
// are different from one tip to another. So, in the appswitcher for
// example, the tooltips will act as root-level z-index 1051. As the chat
// windows are root-level z-index 3, the tooltips will overlap in that case.
//
// Changing the opacity of the tooltip parents forces the creation of a
// stacking context; the appswitcher tooltips are thus now considered to be
// root-level z-index auto (or the default appswitcher one) and should so
// act like their parent (e.g. the appswitcher is below the chat windows so
// the inner tooltips will be too). The tips will be above all elements of
// the appswitcher as they still have a high z-index, but relative to the
// appswitcher (this is especially useful in the website where most tooltips
// are placed in the body and need to be placed above elements with z-index
// like the navbar).
opacity: 0.999;
}
.o_tooltip {
@@ -39,7 +61,7 @@
border: @o-tip-border-width solid @o-tip-color;
border-radius: 50%;
padding: 0; // ! Cannot be changed ! Javascript rely on this.
z-index: @zindex-modal+1;
z-index: @zindex-modal + 1; // See comment on 'o_tooltip_parent' class
background-color: @o-tip-color;
box-shadow: 0 2px 2px 1px rgba(150, 150, 150, 0.3);
@@ -520,52 +520,71 @@ animation.registry.gallery = animation.Class.extend({
animation.registry.gallery_slider = animation.Class.extend({
selector: ".o_slideshow",
start: function () {
var $carousel = this.$target.is(".carousel") ? this.$target : this.$target.find(".carousel");
var $indicator = $carousel.find('.carousel-indicators');
var $lis = $indicator.find('li:not(.fa)');
var $prev = $indicator.find('li.fa:first');
var $next = $indicator.find('li.fa:last');
var index = ($lis.filter('.active').index() || 1) -1;
var page = Math.floor(index / 10);
var nb = Math.ceil($lis.length / 10);
start: function (editable_mode) {
var self = this;
this.$carousel = this.$target.is(".carousel") ? this.$target : this.$target.find(".carousel");
this.$indicator = this.$carousel.find('.carousel-indicators');
this.$prev = this.$indicator.find('li.fa:first').css('visibility', ''); // force visibility as some databases have it hidden
this.$next = this.$indicator.find('li.fa:last').css('visibility', '');
var $lis = this.$indicator.find('li:not(.fa)');
var nbPerPage = Math.floor(this.$indicator.width() / $lis.first().outerWidth(true)) - 3; // - navigator - 1 to leave some space
var realNbPerPage = nbPerPage || 1;
var nbPages = Math.ceil($lis.length / realNbPerPage);
// fix bootstrap use index insead of data-slide-to
$carousel.on('slide.bs.carousel', function () {
var index;
var page;
update();
function hide() {
$lis.each(function (i) {
$(this).toggleClass('hidden', !(i >= page*nbPerPage && i < (page+1)*nbPerPage));
});
if (editable_mode) { // do not remove DOM in edit mode
return;
}
if (page <= 0) {
self.$prev.detach();
} else {
self.$prev.prependTo(self.$indicator);
}
if (page >= nbPages - 1) {
self.$next.detach();
} else {
self.$next.appendTo(self.$indicator);
}
}
function update() {
index = $lis.index($lis.filter('.active')) || 0;
page = Math.floor(index / realNbPerPage);
hide();
}
this.$carousel.on('slide.bs.carousel.gallery_slider', function () {
setTimeout(function () {
var $item = $carousel.find('.carousel-inner .prev, .carousel-inner .next');
var $item = self.$carousel.find('.carousel-inner .prev, .carousel-inner .next');
var index = $item.index();
$lis.removeClass("active")
.filter('[data-slide-to="'+index+'"]')
.addClass("active");
},0);
}, 0);
});
function hide () {
$lis.addClass('hidden').each(function (i) {
if (i >= page*10 && i < (page+1)*10) {
$(this).removeClass('hidden');
}
});
$prev.css('visibility', page === 0 ? 'hidden' : '');
$next.css('visibility', (page+1) >= nb ? 'hidden' : '');
}
$indicator.find('li.fa').on('click', function () {
page = (page + ($(this).hasClass('o_indicators_left')?-1:1)) % nb;
$carousel.carousel(page*10);
this.$indicator.on('click.gallery_slider', '> li.fa', function () {
page += ($(this).hasClass('o_indicators_left') ? -1 : 1);
page = Math.max(0, Math.min(nbPages - 1, page)); // should not be necessary
self.$carousel.carousel(page * realNbPerPage);
hide();
});
hide();
this.$carousel.on('slid.bs.carousel.gallery_slider', update);
},
stop: function () {
this._super.apply(this, arguments);
$carousel.on('slid.bs.carousel', function () {
var index = ($lis.filter('.active').index() || 1) -1;
page = Math.floor(index / 10);
hide();
});
return this._super.apply(this, arguments);
}
this.$prev.prependTo(this.$indicator);
this.$next.appendTo(this.$indicator);
this.$carousel.off('.gallery_slider');
this.$indicator.off('.gallery_slider');
},
});
});
@@ -223,9 +223,6 @@ options.registry.carousel = options.registry.slider.extend({
// set background and prepare to clean for save
this.$target.on('slid.bs.carousel', function () {
self.editor.styles.background_position.$target = self.editor.styles.background.$target;
self.editor.styles.background_position.set_active();
self.editor.styles.background.$target.trigger("snippet-option-change", [self.editor.styles.background]);
self.$target.carousel("pause");
if (!self.editor) return;
@@ -324,9 +324,6 @@ div.carousel .container {
max-height: 100%;
max-width: 100%;
margin: auto;
position: relative;
top: 50%;
.translate(0, -50%);
}
ul.carousel-indicators {
@@ -357,6 +354,10 @@ div.carousel .container {
filter: gray;
filter: url("data:image/svg+xml;utf8,<svg version='1.1' xmlns='http://www.w3.org/2000/svg' height='0'><filter id='greyscale'><feColorMatrix type='matrix' values='0.3333 0.3333 0.3333 0 0 0.3333 0.3333 0.3333 0 0 0.3333 0.3333 0.3333 0 0 0 0 0 1 0' /></filter></svg>#greyscale");
}
&:first-child {
margin-left: 0;
}
}
}
+17 -2
View File
@@ -57,7 +57,14 @@ class Event(models.Model):
if self.event_type_id:
self.website_menu = self.event_type_id.website_menu
def _get_standard_menu_entries_names(self):
""" Method returning menu entries possibly generated by event modules.
It is used to distinguish module-generated pages from custom pages. """
return [_('Introduction'), _('Location'), _('Register')]
def _get_menu_entries(self):
""" Method returning menu entries to display on the website view of the
event, possibly depending on some options in inheriting modules. """
self.ensure_one()
return [
(_('Introduction'), False, 'website_event.template_intro'),
@@ -75,9 +82,17 @@ class Event(models.Model):
root_menu = self.env['website.menu'].create({'name': event.name})
event.menu_id = root_menu
existing_page_names = event.menu_id.child_id.mapped('name')
required_page_names = [entry[0] for entry in self._get_menu_entries()]
standard_page_names = self._get_standard_menu_entries_names()
# remove entries that should not exist anymore
submenu_to_delete = event.menu_id.child_id.filtered(lambda menu: menu.name not in required_page_names and menu.name in standard_page_names)
submenu_to_delete.unlink()
# create missing entries
for sequence, (name, url, xml_id) in enumerate(self._get_menu_entries()):
existing_pages = event.menu_id.child_id.mapped('name')
if name not in existing_pages:
if name not in existing_page_names:
if not url:
newpath = self.env['website'].new_page(name + ' ' + self.name, xml_id, ispage=False)
url = "/event/" + slug(self) + "/page/" + newpath
@@ -80,6 +80,21 @@ class Event(models.Model):
self.website_track = False
self.website_track_proposal = False
@api.onchange('website_track')
def _onchange_website_track(self):
if not self.website_track:
self.website_track_proposal = False
@api.onchange('website_track_proposal')
def _onchange_website_track_proposal(self):
if self.website_track_proposal:
self.website_track = True
def _get_standard_menu_entries_names(self):
res = super(Event, self)._get_standard_menu_entries_names()
res += [_('Talks'), _('Agenda'), _('Talk Proposals')]
return res
def _get_menu_entries(self):
self.ensure_one()
res = super(Event, self)._get_menu_entries()
+1 -1
View File
@@ -171,7 +171,7 @@ class WebsiteForm(http.Controller):
return data
def insert_record(self, request, model, values, custom, meta=None):
record = request.env[model.model].sudo().create(values)
record = request.env[model.model].sudo().with_context(mail_create_nosubscribe=True).create(values)
if custom or meta:
default_field = model.website_form_default_field_id
@@ -1,6 +1,8 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import werkzeug
from datetime import datetime
from dateutil import relativedelta
@@ -119,6 +121,9 @@ class MailGroup(http.Controller):
"/groups/<model('mail.channel'):group>/page/<int:page>"
], type='http', auth="public", website=True)
def thread_headers(self, group, page=1, mode='thread', date_begin=None, date_end=None, **post):
if group.channel_type != 'channel':
raise werkzeug.exceptions.NotFound()
Message = request.env['mail.message']
domain = [('model', '=', 'mail.channel'), ('res_id', '=', group.id), ('message_type', '!=', 'notification')]
@@ -151,6 +156,9 @@ class MailGroup(http.Controller):
'''/groups/<model('mail.channel'):group>/<model('mail.message', "[('model','=','mail.channel'), ('res_id','=',group[0])]"):message>''',
], type='http', auth="public", website=True)
def thread_discussion(self, group, message, mode='thread', date_begin=None, date_end=None, **post):
if group.channel_type != 'channel':
raise werkzeug.exceptions.NotFound()
Message = request.env['mail.message']
if mode == 'thread':
base_domain = [('model', '=', 'mail.channel'), ('res_id', '=', group.id), ('parent_id', '=', message.parent_id and message.parent_id.id or False)]
@@ -175,6 +183,9 @@ class MailGroup(http.Controller):
'''/groups/<model('mail.channel'):group>/<model('mail.message', "[('model','=','mail.channel'), ('res_id','=',group[0])]"):message>/get_replies''',
type='json', auth="public", methods=['POST'], website=True)
def render_messages(self, group, message, **post):
if group.channel_type != 'channel':
return False
last_displayed_id = post.get('last_displayed_id')
if not last_displayed_id:
return False
+2 -2
View File
@@ -465,7 +465,7 @@
<a id="add_to_cart" class="btn btn-primary btn-lg mt8 js_check_product a-submit" href="#">Add to Cart</a>
</div>
</form>
<hr />
<hr t-if="product.availability != 'empty'"/>
<div t-if="product.availability == 'in_stock'" class="alert alert-success">
<span class="fa fa-check" />
<span>In Stock</span>
@@ -1431,7 +1431,7 @@
<div class="col-sm-12">
<form target="_self" action="/shop/payment/validate" method="post" class="pull-right">
<input type="hidden" name="csrf_token" t-att-value="request.csrf_token()" />
<a style="width:127px;" class="btn btn-primary a-submit">
<a class="btn btn-primary a-submit">
<span t-if="order.amount_total &gt; 0">Pay Now <span class="fa fa-long-arrow-right"></span></span>
<span t-if="order.amount_total == 0">Confirm Order <span class="fa fa-long-arrow-right"></span></span>
</a>
+11
View File
@@ -0,0 +1,11 @@
Morocco, 2016-06-26
I hereby agree to the terms of the Odoo Individual Contributor License
Agreement v1.0.
I declare that I am authorized and able to make this agreement and sign this
declaration.
Signed,
Ayoub Zahid azahid@agilorg.com https://github.com/AyoubZahid
+9
View File
@@ -0,0 +1,9 @@
Germany, 2017-04-27
I hereby agree to the terms of the Odoo Individual Contributor License Agreement v1.0.
I declare that I am authorized and able to make this agreement and sign this declaration.
Signed,
Felix Kopp fxkopp@users.noreply.github.com https://github.com/fxkopp
+10
View File
@@ -0,0 +1,10 @@
Germany, 2017-05-09
I hereby agree to the terms of the Odoo Individual Contributor License Agreement v1.0.
I declare that I am authorized and able to make this agreement and sign this declaration.
Signed,
Josef Kaser josef.kaser@pragmasoft.de https://github.com/josefkaser
+11
View File
@@ -0,0 +1,11 @@
France, 2017/05/10
I hereby agree to the terms of the Odoo Individual Contributor License
Agreement v1.0.
I declare that I am authorized and able to make this agreement and sign this
declaration.
Signed,
Xavier Brochard xavier@alternatif.org< https://github.com/zeroheure
+12 -13
View File
@@ -50,7 +50,7 @@ Composition of a module
An Odoo module can contain a number of elements:
Business objects
declared as Python classes, these resources are automatically persisted
Declared as Python classes, these resources are automatically persisted
by Odoo based on their configuration
Data files
@@ -77,8 +77,7 @@ option.
file <reference/cmdline/config>`
An Odoo module is declared by its :ref:`manifest <reference/module/manifest>`.
See the :ref:`manifest documentation <reference/module/manifest>` information
about it.
See the :ref:`manifest documentation <reference/module/manifest>` about it.
A module is also a
`Python package <http://docs.python.org/2/tutorial/modules.html#packages>`_
@@ -167,7 +166,7 @@ Some attributes are available on all fields, here are the most common ones:
:attr:`~odoo.fields.Field.help` (``unicode``, default: ``''``)
Long-form, provides a help tooltip to users in the UI.
:attr:`~odoo.fields.Field.index` (``bool``, default: ``False``)
Requests that Odoo create a `database index`_ on the column
Requests that Odoo create a `database index`_ on the column.
Simple fields
#############
@@ -187,15 +186,15 @@ managed by the system and shouldn't be written to. They can be read if
useful or necessary:
:attr:`~odoo.fields.Model.id` (:class:`~odoo.fields.Id`)
the unique identifier for a record in its model
The unique identifier for a record in its model.
:attr:`~odoo.fields.Model.create_date` (:class:`~odoo.fields.Datetime`)
creation date of the record
Creation date of the record.
:attr:`~odoo.fields.Model.create_uid` (:class:`~odoo.fields.Many2one`)
user who created the record
User who created the record.
:attr:`~odoo.fields.Model.write_date` (:class:`~odoo.fields.Datetime`)
last modification date of the record
Last modification date of the record.
:attr:`~odoo.fields.Model.write_uid` (:class:`~odoo.fields.Many2one`)
user who last modified the record
user who last modified the record.
Special fields
##############
@@ -238,9 +237,9 @@ record.
</data>
</odoo>
* ``model`` is the name of the Odoo model for the record
* ``model`` is the name of the Odoo model for the record.
* ``id`` is an :term:`external identifier`, it allows referring to the record
(without having to know its in-database identifier)
(without having to know its in-database identifier).
* ``<field>`` elements have a ``name`` which is the name of the field in the
model (e.g. ``description``). Their body is the field's value.
@@ -294,7 +293,7 @@ action more easily.
.. exercise:: Define new menu entries
Define new menu entries to access courses under the
OpenAcademy menu entry. A user should be able to
OpenAcademy menu entry. A user should be able to :
- display a list of all the courses
- create/modify courses
@@ -365,7 +364,7 @@ Form views
Forms are used to create and edit single records.
Their root element is ``<form>``. They composed of high-level structure
Their root element is ``<form>``. They are composed of high-level structure
elements (groups, notebooks) and interactive elements (buttons and fields):
.. code-block:: xml
+3 -1
View File
@@ -67,6 +67,8 @@
<field name="user_id"/>
<field name="nextcall"/>
<field name="active"/>
<separator/>
<filter string="All" name="all" domain="['|', ('active', '=', False), ('active', '=', True)]" />
<group expand="0" string="Group By">
<filter string="User" domain="[]" context="{'group_by':'user_id'}"/>
<filter string="Execution" domain="[]" context="{'group_by':'nextcall'}" />
@@ -79,7 +81,7 @@
<field name="res_model">ir.cron</field>
<field name="view_type">form</field>
<field name="view_mode">tree,form,calendar</field>
<field name="context">{'active_test': False}</field>
<field name="context">{'search_default_all': 1}</field>
<field name="view_id" ref="ir_cron_view_tree"/>
</record>
<menuitem id="menu_ir_cron_act" action="ir_cron_act" parent="base.menu_automation"/>
+1 -1
View File
@@ -2263,7 +2263,7 @@ class BaseModel(object):
cls = type(self)
cls._setup_done = False
# a model's base structure depends on its mro (without registry classes)
cls._model_cache_key = tuple(c for c in cls.mro() if not getattr(c, 'pool', None))
cls._model_cache_key = tuple(c for c in cls.mro() if getattr(c, 'pool', None) is None)
@api.model
def _setup_base(self):
+21 -9
View File
@@ -342,27 +342,39 @@ class GeventServer(CommonServer):
self.port = config['longpolling_port']
self.httpd = None
def watch_parent(self, beat=4):
def process_limits(self):
restart = False
if self.ppid != os.getppid():
_logger.warning("LongPolling Parent changed", self.pid)
restart = True
rss, vms = memory_info(psutil.Process(self.pid))
if vms > config['limit_memory_soft']:
_logger.warning('LongPolling virtual memory limit reached: %s', vms)
restart = True
if restart:
# suicide !!
os.kill(self.pid, signal.SIGTERM)
def watchdog(self, beat=4):
import gevent
ppid = os.getppid()
self.ppid = os.getppid()
while True:
if ppid != os.getppid():
pid = os.getpid()
_logger.info("LongPolling (%s) Parent changed", pid)
# suicide !!
os.kill(pid, signal.SIGTERM)
return
self.process_limits()
gevent.sleep(beat)
def start(self):
import gevent
from gevent.wsgi import WSGIServer
# Set process memory limit as an extra safeguard
_, hard = resource.getrlimit(resource.RLIMIT_AS)
resource.setrlimit(resource.RLIMIT_AS, (config['limit_memory_hard'], hard))
if os.name == 'posix':
signal.signal(signal.SIGQUIT, dumpstacks)
signal.signal(signal.SIGUSR1, log_ormcache_stats)
gevent.spawn(self.watch_parent)
gevent.spawn(self.watchdog)
self.httpd = WSGIServer((self.interface, self.port), self.app)
_logger.info('Evented Service (longpolling) running on %s:%s', self.interface, self.port)
try:
+1 -1
View File
@@ -198,7 +198,7 @@ def log_ormcache_stats(sig=None, frame=None):
import threading
me = threading.currentThread()
me_dbname = me.dbname
me_dbname = getattr(me, 'dbname', 'n/a')
entries = defaultdict(int)
for dbname, reg in Registry.registries.iteritems():
for key in reg.cache.iterkeys():
+4
View File
@@ -10,6 +10,10 @@ from PIL import Image
from PIL import ImageEnhance
from random import randrange
# Preload PIL with the minimal subset of image formats we need
Image.preinit()
Image._initialized = 2
# ----------------------------------------
# Image resizing
# ----------------------------------------