[MERGE] Forward-port saas-15 up to 878fbc75ff
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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/>
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 = {}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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) <= 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,
|
||||
};
|
||||
|
||||
});
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,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>
|
||||
|
||||
@@ -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"/>
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 ??
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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')]}"/>
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 > 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>
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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():
|
||||
|
||||
@@ -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
|
||||
# ----------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user