[MERGE] forward port branch 10.0 up to 6804085b8c
This commit is contained in:
@@ -570,7 +570,7 @@ class AccountInvoice(models.Model):
|
||||
# lots of duplicate calls to action_invoice_paid, so we remove those already paid
|
||||
to_pay_invoices = self.filtered(lambda inv: inv.state != 'paid')
|
||||
if to_pay_invoices.filtered(lambda inv: inv.state != 'open'):
|
||||
raise UserError(_('Invoice must be validated in order to set it to register payemnt.'))
|
||||
raise UserError(_('Invoice must be validated in order to set it to register payment.'))
|
||||
if to_pay_invoices.filtered(lambda inv: not inv.reconciled):
|
||||
raise UserError(_('You cannot pay an invoice which is partially paid. You need to reconcile payment entries first.'))
|
||||
return to_pay_invoices.write({'state': 'paid'})
|
||||
@@ -578,13 +578,13 @@ class AccountInvoice(models.Model):
|
||||
@api.multi
|
||||
def action_invoice_re_open(self):
|
||||
if self.filtered(lambda inv: inv.state != 'paid'):
|
||||
raise UserError(_('Invoice must be paid in order to set it to register payemnt.'))
|
||||
raise UserError(_('Invoice must be paid in order to set it to register payment.'))
|
||||
return self.write({'state': 'open'})
|
||||
|
||||
@api.multi
|
||||
def action_invoice_cancel(self):
|
||||
if self.filtered(lambda inv: inv.state not in ['proforma2', 'draft', 'open']):
|
||||
raise UserError(_("Invoice must be in draft,Pro-forma or open state in order to be cancelled."))
|
||||
raise UserError(_("Invoice must be in draft, Pro-forma or open state in order to be cancelled."))
|
||||
return self.action_cancel()
|
||||
|
||||
@api.multi
|
||||
@@ -1151,7 +1151,8 @@ class AccountInvoiceLine(models.Model):
|
||||
|
||||
@api.one
|
||||
@api.depends('price_unit', 'discount', 'invoice_line_tax_ids', 'quantity',
|
||||
'product_id', 'invoice_id.partner_id', 'invoice_id.currency_id', 'invoice_id.company_id')
|
||||
'product_id', 'invoice_id.partner_id', 'invoice_id.currency_id', 'invoice_id.company_id',
|
||||
'invoice_id.date_invoice')
|
||||
def _compute_price(self):
|
||||
currency = self.invoice_id and self.invoice_id.currency_id or None
|
||||
price = self.price_unit * (1 - (self.discount or 0.0) / 100.0)
|
||||
@@ -1160,7 +1161,7 @@ class AccountInvoiceLine(models.Model):
|
||||
taxes = self.invoice_line_tax_ids.compute_all(price, currency, self.quantity, product=self.product_id, partner=self.invoice_id.partner_id)
|
||||
self.price_subtotal = price_subtotal_signed = taxes['total_excluded'] if taxes else self.quantity * price
|
||||
if self.invoice_id.currency_id and self.invoice_id.company_id and self.invoice_id.currency_id != self.invoice_id.company_id.currency_id:
|
||||
price_subtotal_signed = self.invoice_id.currency_id.compute(price_subtotal_signed, self.invoice_id.company_id.currency_id)
|
||||
price_subtotal_signed = self.invoice_id.currency_id.with_context(date=self.invoice_id.date_invoice).compute(price_subtotal_signed, self.invoice_id.company_id.currency_id)
|
||||
sign = self.invoice_id.type in ['in_refund', 'out_refund'] and -1 or 1
|
||||
self.price_subtotal_signed = price_subtotal_signed * sign
|
||||
|
||||
|
||||
@@ -394,7 +394,7 @@ class AccountMoveLine(models.Model):
|
||||
statement_id = fields.Many2one('account.bank.statement', related='statement_line_id.statement_id', string='Statement', store=True,
|
||||
help="The bank statement used for bank reconciliation", index=True, copy=False)
|
||||
reconciled = fields.Boolean(compute='_amount_residual', store=True)
|
||||
full_reconcile_id = fields.Many2one('account.full.reconcile', string="Matching Number")
|
||||
full_reconcile_id = fields.Many2one('account.full.reconcile', string="Matching Number", copy=False)
|
||||
matched_debit_ids = fields.One2many('account.partial.reconcile', 'credit_move_id', String='Matched Debits',
|
||||
help='Debit journal items that are matched with this journal item.')
|
||||
matched_credit_ids = fields.One2many('account.partial.reconcile', 'debit_move_id', String='Matched Credits',
|
||||
@@ -1020,7 +1020,7 @@ class AccountMoveLine(models.Model):
|
||||
rec_move_ids = self.env['account.partial.reconcile']
|
||||
for account_move_line in self:
|
||||
for invoice in account_move_line.payment_id.invoice_ids:
|
||||
if account_move_line in invoice.payment_move_line_ids:
|
||||
if invoice.id == self.env.context.get('invoice_id') and account_move_line in invoice.payment_move_line_ids:
|
||||
account_move_line.payment_id.write({'invoice_ids': [(3, invoice.id, None)]})
|
||||
rec_move_ids += account_move_line.matched_debit_ids
|
||||
rec_move_ids += account_move_line.matched_credit_ids
|
||||
@@ -1388,7 +1388,7 @@ class AccountPartialReconcile(models.Model):
|
||||
company_currency_id = fields.Many2one('res.currency', related='company_id.currency_id', readonly=True,
|
||||
help='Utility field to express amount currency')
|
||||
company_id = fields.Many2one('res.company', related='debit_move_id.company_id', store=True, string='Currency')
|
||||
full_reconcile_id = fields.Many2one('account.full.reconcile', string="Full Reconcile")
|
||||
full_reconcile_id = fields.Many2one('account.full.reconcile', string="Full Reconcile", copy=False)
|
||||
|
||||
def create_exchange_rate_entry(self, aml_to_fix, amount_diff, diff_in_currency, currency, move_date):
|
||||
""" Automatically create a journal entry to book the exchange rate difference.
|
||||
|
||||
@@ -400,9 +400,12 @@ class account_payment(models.Model):
|
||||
# the writeoff debit and credit must be computed from the invoice residual in company currency
|
||||
# minus the payment amount in company currency, and not from the payment difference in the payment currency
|
||||
# to avoid loss of precision during the currency rate computations. See revision 20935462a0cabeb45480ce70114ff2f4e91eaf79 for a detailed example.
|
||||
total_residual_company_signed = sum(invoice.residual_company_signed for invoice in self.invoice_ids)
|
||||
total_residual_company_signed = self._compute_total_invoices_amount()
|
||||
total_payment_company_signed = self.currency_id.with_context(date=self.payment_date).compute(self.amount, self.company_id.currency_id)
|
||||
amount_wo = total_residual_company_signed - total_payment_company_signed
|
||||
if self.invoice_ids[0].type in ['in_invoice', 'out_refund']:
|
||||
amount_wo = total_payment_company_signed - total_residual_company_signed
|
||||
else:
|
||||
amount_wo = total_residual_company_signed - total_payment_company_signed
|
||||
debit_wo = amount_wo > 0 and amount_wo or 0.0
|
||||
credit_wo = amount_wo < 0 and -amount_wo or 0.0
|
||||
writeoff_line['name'] = _('Counterpart')
|
||||
|
||||
@@ -394,22 +394,11 @@ class AccountChartTemplate(models.Model):
|
||||
acc_template_ref[account_template.id] = new_account
|
||||
return acc_template_ref
|
||||
|
||||
@api.multi
|
||||
def generate_account_reconcile_model(self, tax_template_ref, acc_template_ref, company):
|
||||
""" This method for generating accounts from templates.
|
||||
|
||||
:param tax_template_ref: Taxes templates reference for write taxes_id in account_account.
|
||||
:param acc_template_ref: dictionary with the mappping between the account templates and the real accounts.
|
||||
:param company_id: company_id selected from wizard.multi.charts.accounts.
|
||||
:returns: return new_account_reconcile_model for reference purpose.
|
||||
:rtype: dict
|
||||
def _prepare_reconcile_model_vals(self, company, account_reconcile_model, acc_template_ref, tax_template_ref):
|
||||
""" This method generates a dictionnary of all the values for the account.reconcile.model that will be created.
|
||||
"""
|
||||
self.ensure_one()
|
||||
account_reconcile_models = self.env['account.reconcile.model.template'].search([
|
||||
('account_id.chart_template_id', '=', self.id)
|
||||
])
|
||||
for account_reconcile_model in account_reconcile_models:
|
||||
vals = {
|
||||
return {
|
||||
'name': account_reconcile_model.name,
|
||||
'sequence': account_reconcile_model.sequence,
|
||||
'has_second_line': account_reconcile_model.has_second_line,
|
||||
@@ -425,6 +414,23 @@ class AccountChartTemplate(models.Model):
|
||||
'second_amount': account_reconcile_model.second_amount,
|
||||
'second_tax_id': account_reconcile_model.second_tax_id and tax_template_ref[account_reconcile_model.second_tax_id.id] or False,
|
||||
}
|
||||
|
||||
@api.multi
|
||||
def generate_account_reconcile_model(self, tax_template_ref, acc_template_ref, company):
|
||||
""" This method for generating accounts from templates.
|
||||
|
||||
:param tax_template_ref: Taxes templates reference for write taxes_id in account_account.
|
||||
:param acc_template_ref: dictionary with the mappping between the account templates and the real accounts.
|
||||
:param company_id: company_id selected from wizard.multi.charts.accounts.
|
||||
:returns: return new_account_reconcile_model for reference purpose.
|
||||
:rtype: dict
|
||||
"""
|
||||
self.ensure_one()
|
||||
account_reconcile_models = self.env['account.reconcile.model.template'].search([
|
||||
('account_id.chart_template_id', '=', self.id)
|
||||
])
|
||||
for account_reconcile_model in account_reconcile_models:
|
||||
vals = self._prepare_reconcile_model_vals(company, account_reconcile_model, acc_template_ref, tax_template_ref)
|
||||
self.create_record_with_xmlid(company, account_reconcile_model, 'account.reconcile.model', vals)
|
||||
return True
|
||||
|
||||
|
||||
@@ -5,10 +5,14 @@
|
||||
<record id="group_account_invoice" model="res.groups">
|
||||
<field name="name">Billing</field>
|
||||
<field name="category_id" ref="base.module_category_accounting_and_finance"/>
|
||||
<field name="users" eval="[(4, ref('base.user_root'))]"/>
|
||||
<field name="implied_ids" eval="[(4, ref('base.group_user'))]"/>
|
||||
</record>
|
||||
|
||||
<!-- 2 steps to avoid making reference to xml id not defined yet -->
|
||||
<record id="group_account_invoice" model="res.groups">
|
||||
<field name="users" eval="[(4, ref('base.user_root'))]"/>
|
||||
</record>
|
||||
|
||||
<record id="group_account_user" model="res.groups">
|
||||
<field name="name">Accountant</field>
|
||||
<field name="category_id" ref="base.module_category_accounting_and_finance"/>
|
||||
|
||||
@@ -59,7 +59,7 @@ var ShowPaymentLineWidget = form_common.AbstractField.extend({
|
||||
var payment_id = parseInt($(this).attr('payment-id'))
|
||||
if (payment_id !== undefined && payment_id !== NaN){
|
||||
new Model("account.move.line")
|
||||
.call("remove_move_reconcile", [payment_id])
|
||||
.call("remove_move_reconcile", [payment_id, {'invoice_id': self.view.datarecord.id}])
|
||||
.then(function (result) {
|
||||
self.view.reload();
|
||||
});
|
||||
|
||||
@@ -46,7 +46,7 @@
|
||||
<field name="quantity"/>
|
||||
<field name="uom_id" groups="product.group_uom"/>
|
||||
<field name="price_unit"/>
|
||||
<field name="discount" groups="sale.group_discount_per_so_line"/>
|
||||
<field name="discount" groups="base.group_no_one"/>
|
||||
<field name="price_subtotal"/>
|
||||
<field name="currency_id" invisible="1"/>
|
||||
</tree>
|
||||
@@ -68,7 +68,7 @@
|
||||
<field name="uom_id" class="oe_inline" groups="product.group_uom"/>
|
||||
</div>
|
||||
<field name="price_unit"/>
|
||||
<field name="discount" groups="sale.group_discount_per_so_line"/>
|
||||
<field name="discount" groups="base.group_no_one"/>
|
||||
<field name="currency_id" invisible="1"/>
|
||||
</group>
|
||||
<group>
|
||||
@@ -272,7 +272,7 @@
|
||||
<field name="quantity"/>
|
||||
<field name="uom_id" groups="product.group_uom"/>
|
||||
<field name="price_unit"/>
|
||||
<field name="discount" groups="sale.group_discount_per_so_line"/>
|
||||
<field name="discount" groups="base.group_no_one"/>
|
||||
<field name="invoice_line_tax_ids" widget="many2many_tags" context="{'type':parent.type}"
|
||||
domain="[('type_tax_use','=','purchase'),('company_id', '=', parent.company_id)]" options="{'no_create': True}"/>
|
||||
<field name="price_subtotal"/>
|
||||
@@ -411,7 +411,7 @@
|
||||
<field name="quantity"/>
|
||||
<field name="uom_id" groups="product.group_uom"/>
|
||||
<field name="price_unit"/>
|
||||
<field name="discount" groups="sale.group_discount_per_so_line"/>
|
||||
<field name="discount" groups="base.group_no_one"/>
|
||||
<field name="invoice_line_tax_ids" widget="many2many_tags" context="{'type':parent.type}"
|
||||
domain="[('type_tax_use','=','sale'),('company_id', '=', parent.company_id)]" options="{'no_create': True}"/>
|
||||
<field name="price_subtotal"/>
|
||||
|
||||
@@ -61,7 +61,7 @@
|
||||
<th class="hidden">Source Document</th>
|
||||
<th class="text-right">Quantity</th>
|
||||
<th class="text-right">Unit Price</th>
|
||||
<th t-if="display_discount" class="text-right" groups="sale.group_discount_per_so_line">Disc.(%)</th>
|
||||
<th t-if="display_discount" class="text-right">Disc.(%)</th>
|
||||
<th class="text-right">Taxes</th>
|
||||
<th class="text-right">Subtotal</th>
|
||||
</tr>
|
||||
@@ -77,7 +77,7 @@
|
||||
<td class="text-right">
|
||||
<span t-field="l.price_unit"/>
|
||||
</td>
|
||||
<td t-if="display_discount" class="text-right" groups="sale.group_discount_per_so_line">
|
||||
<td t-if="display_discount" class="text-right">
|
||||
<span t-field="l.discount"/>
|
||||
</td>
|
||||
<td class="text-right">
|
||||
|
||||
@@ -42,7 +42,10 @@ class AccountAssetCategory(models.Model):
|
||||
|
||||
@api.onchange('account_asset_id')
|
||||
def onchange_account_asset(self):
|
||||
self.account_depreciation_id = self.account_asset_id
|
||||
if self.type == "purchase":
|
||||
self.account_depreciation_id = self.account_asset_id
|
||||
elif self.type == "sale":
|
||||
self.account_depreciation_expense_id = self.account_asset_id
|
||||
|
||||
@api.onchange('type')
|
||||
def onchange_type(self):
|
||||
|
||||
@@ -12,14 +12,16 @@ import odoo.addons.decimal_precision as dp
|
||||
class AccountInvoice(models.Model):
|
||||
_inherit = 'account.invoice'
|
||||
|
||||
@api.multi
|
||||
def action_cancel(self):
|
||||
res = super(AccountInvoice, self).action_cancel()
|
||||
self.env['account.asset.asset'].sudo().search([('invoice_id', 'in', self.ids)]).write({'active': False})
|
||||
return res
|
||||
|
||||
@api.multi
|
||||
def action_move_create(self):
|
||||
result = super(AccountInvoice, self).action_move_create()
|
||||
for inv in self:
|
||||
if inv.number:
|
||||
asset_ids = self.env['account.asset.asset'].sudo().search([('invoice_id', '=', inv.id), ('company_id', '=', inv.company_id.id)])
|
||||
if asset_ids:
|
||||
asset_ids.write({'active': False})
|
||||
context = dict(self.env.context)
|
||||
# Within the context of an invoice,
|
||||
# this default value is for the type of the invoice, not the type of the asset.
|
||||
@@ -83,7 +85,7 @@ class AccountInvoiceLine(models.Model):
|
||||
if self.invoice_id.type == 'out_invoice' and self.asset_category_id:
|
||||
self.account_id = self.asset_category_id.account_asset_id.id
|
||||
elif self.invoice_id.type == 'in_invoice' and self.asset_category_id:
|
||||
self.account_id = self.asset_category_id.account_depreciation_id.id
|
||||
self.account_id = self.asset_category_id.account_asset_id.id
|
||||
|
||||
@api.onchange('uom_id')
|
||||
def _onchange_uom_id(self):
|
||||
|
||||
@@ -88,7 +88,7 @@ class AccountPayment(models.Model):
|
||||
and vals.get('check_manual_sequencing'):
|
||||
sequence = self.env['account.journal'].browse(vals['journal_id']).check_sequence_id
|
||||
vals.update({'check_number': sequence.next_by_id()})
|
||||
return super(AccountPayment, self.sudo()).create(vals)
|
||||
return super(AccountPayment, self).create(vals)
|
||||
|
||||
@api.multi
|
||||
def print_checks(self):
|
||||
|
||||
@@ -111,8 +111,6 @@ class OAuthLogin(Home):
|
||||
@http.route()
|
||||
def web_auth_signup(self, *args, **kw):
|
||||
providers = self.list_providers()
|
||||
if len(providers) == 1:
|
||||
werkzeug.exceptions.abort(werkzeug.utils.redirect(providers[0]['auth_link'], 303))
|
||||
response = super(OAuthLogin, self).web_auth_signup(*args, **kw)
|
||||
response.qcontext.update(providers=providers)
|
||||
return response
|
||||
|
||||
@@ -13,7 +13,7 @@ class Board(Controller):
|
||||
# Retrieve the 'My Dashboard' action from its xmlid
|
||||
action = request.env.ref('board.open_board_my_dash_action')
|
||||
|
||||
if action and action['res_model'] == 'board.board' and action['views'][0][1] == 'form':
|
||||
if action and action['res_model'] == 'board.board' and action['views'][0][1] == 'form' and action_id:
|
||||
# Maybe should check the content instead of model board.board ?
|
||||
view_id = action['views'][0][0]
|
||||
board = request.env['board.board'].fields_view_get(view_id, 'form')
|
||||
|
||||
@@ -413,7 +413,7 @@ FavoriteMenu.include({
|
||||
name = self.$add_dashboard_input.val();
|
||||
|
||||
return self.rpc('/board/add_to_dashboard', {
|
||||
action_id: self.action_id,
|
||||
action_id: self.action_id || false,
|
||||
context_to_save: c,
|
||||
domain: d,
|
||||
view_mode: self.view_manager.active_view.type,
|
||||
|
||||
@@ -34,24 +34,30 @@ class ResPartner(models.Model):
|
||||
:param name : the partner name to search
|
||||
:param limit : the limit of result to return
|
||||
"""
|
||||
name = '%' + name + '%'
|
||||
excluded_partner_ids = [self.env.user.partner_id.id]
|
||||
self.env.cr.execute("""
|
||||
SELECT
|
||||
U.id as user_id,
|
||||
P.id as id,
|
||||
P.name as name,
|
||||
CASE WHEN B.last_poll IS NULL THEN 'offline'
|
||||
WHEN age(now() AT TIME ZONE 'UTC', B.last_poll) > interval %s THEN 'offline'
|
||||
WHEN age(now() AT TIME ZONE 'UTC', B.last_presence) > interval %s THEN 'away'
|
||||
ELSE 'online'
|
||||
END as im_status
|
||||
FROM res_users U
|
||||
JOIN res_partner P ON P.id = U.partner_id
|
||||
LEFT JOIN bus_presence B ON B.user_id = U.id
|
||||
WHERE P.name ILIKE %s
|
||||
AND P.id NOT IN %s
|
||||
AND U.active = 't'
|
||||
LIMIT %s
|
||||
""", ("%s seconds" % DISCONNECTION_TIMER, "%s seconds" % AWAY_TIMER, name, tuple(excluded_partner_ids), limit))
|
||||
return self.env.cr.dictfetchall()
|
||||
# This method is supposed to be used only in the context of channel creation or
|
||||
# extension via an invite. As both of these actions require the 'create' access
|
||||
# right, we check this specific ACL.
|
||||
if self.env['mail.channel'].check_access_rights('create', raise_exception=False):
|
||||
name = '%' + name + '%'
|
||||
excluded_partner_ids = [self.env.user.partner_id.id]
|
||||
self.env.cr.execute("""
|
||||
SELECT
|
||||
U.id as user_id,
|
||||
P.id as id,
|
||||
P.name as name,
|
||||
CASE WHEN B.last_poll IS NULL THEN 'offline'
|
||||
WHEN age(now() AT TIME ZONE 'UTC', B.last_poll) > interval %s THEN 'offline'
|
||||
WHEN age(now() AT TIME ZONE 'UTC', B.last_presence) > interval %s THEN 'away'
|
||||
ELSE 'online'
|
||||
END as im_status
|
||||
FROM res_users U
|
||||
JOIN res_partner P ON P.id = U.partner_id
|
||||
LEFT JOIN bus_presence B ON B.user_id = U.id
|
||||
WHERE P.name ILIKE %s
|
||||
AND P.id NOT IN %s
|
||||
AND U.active = 't'
|
||||
LIMIT %s
|
||||
""", ("%s seconds" % DISCONNECTION_TIMER, "%s seconds" % AWAY_TIMER, name, tuple(excluded_partner_ids), limit))
|
||||
return self.env.cr.dictfetchall()
|
||||
else:
|
||||
return {}
|
||||
|
||||
@@ -173,6 +173,7 @@ class Attendee(models.Model):
|
||||
'datas_fname': 'invitation.ics',
|
||||
'datas': str(ics_file).encode('base64')})]
|
||||
vals['model'] = None # We don't want to have the mail in the tchatter while in queue!
|
||||
vals['res_id'] = False
|
||||
current_mail = self.env['mail.mail'].browse(mail_id)
|
||||
current_mail.mail_message_id.write(vals)
|
||||
mails_to_send |= current_mail
|
||||
@@ -1360,11 +1361,11 @@ class Meeting(models.Model):
|
||||
super(Meeting, real_meetings).write(values)
|
||||
|
||||
# set end_date for calendar searching
|
||||
if values.get('recurrency') and values.get('end_type', 'count') in ('count', unicode('count')) and \
|
||||
(values.get('rrule_type') or values.get('count') or values.get('start') or values.get('stop')):
|
||||
if any(field in values for field in ['recurrency', 'end_type', 'count', 'rrule_type', 'start', 'stop']):
|
||||
for real_meeting in real_meetings:
|
||||
final_date = real_meeting._get_recurrency_end_date()
|
||||
super(Meeting, real_meeting).write({'final_date': final_date})
|
||||
if real_meeting.recurrency and real_meeting.end_type in ('count', unicode('count')):
|
||||
final_date = real_meeting._get_recurrency_end_date()
|
||||
super(Meeting, real_meeting).write({'final_date': final_date})
|
||||
|
||||
attendees_create = False
|
||||
if values.get('partner_ids', False):
|
||||
@@ -1464,7 +1465,8 @@ class Meeting(models.Model):
|
||||
for r in result:
|
||||
if r['user_id']:
|
||||
user_id = type(r['user_id']) in (tuple, list) and r['user_id'][0] or r['user_id']
|
||||
if user_id == self.env.user.id:
|
||||
partner_id = self.env.user.partner_id.id
|
||||
if user_id == self.env.user.id or partner_id in r.get("partner_ids", []):
|
||||
continue
|
||||
if r['privacy'] == 'private':
|
||||
for f in r.keys():
|
||||
|
||||
@@ -23,5 +23,15 @@
|
||||
<field name="groups" eval="[(4, ref('base.group_portal'))]"/>
|
||||
</record>
|
||||
|
||||
<record id="calendar_event_rule_private" model="ir.rule">
|
||||
<field ref="model_calendar_event" name="model_id"/>
|
||||
<field name="name">Private events</field>
|
||||
<field name="domain_force">['|', ('privacy', '!=', 'private'), '&', ('privacy', '=', 'private'), ('partner_ids', 'in', user.partner_id.id)]</field>
|
||||
<field name="perm_read" eval="False"/>
|
||||
<field name="perm_write" eval="True"/>
|
||||
<field name="perm_create" eval="True"/>
|
||||
<field name="perm_unlink" eval="True"/>
|
||||
</record>
|
||||
|
||||
</data>
|
||||
</odoo>
|
||||
|
||||
@@ -40,6 +40,9 @@ Dashboard for CRM will include:
|
||||
'web_tour',
|
||||
],
|
||||
'data': [
|
||||
'security/crm_security.xml',
|
||||
'security/ir.model.access.csv',
|
||||
|
||||
'data/crm_data.xml',
|
||||
'data/crm_stage_data.xml',
|
||||
'data/sales_config_settings_data.xml',
|
||||
@@ -47,9 +50,6 @@ Dashboard for CRM will include:
|
||||
'data/web_planner_data.xml',
|
||||
'data/mail_template_data.xml',
|
||||
|
||||
'security/crm_security.xml',
|
||||
'security/ir.model.access.csv',
|
||||
|
||||
'wizard/base_partner_merge_views.xml',
|
||||
'wizard/crm_lead_lost_views.xml',
|
||||
'wizard/crm_lead_to_opportunity_views.xml',
|
||||
|
||||
@@ -91,11 +91,10 @@
|
||||
<span style="font-style:italic;">You’re not alone</span>
|
||||
</p>
|
||||
<p dir="ltr" style="font-size:13px;margin-top:0pt;margin-bottom:8pt;">
|
||||
<span>We would be delighted to assist you along the way. Contact us at
|
||||
<a href="mailto:help@odoo.com"><span style="text-decoration:underline;">
|
||||
help@odoo.com</span></a> if you have any question. You can also discover
|
||||
how to get the best out of Odoo CRM with our </span>
|
||||
<a href="https://www.odoo.com/documentation/user/9.0/crm.html">
|
||||
<span>We would be delighted to assist you along the way. Contact us through our
|
||||
<a href="https://www.odoo.com/help">support form</a> if you have any question.
|
||||
You can also discover how to get the best out of Odoo CRM with our </span>
|
||||
<a href="https://www.odoo.com/documentation/user/10.0/crm.html">
|
||||
<span style="text-decoration:underline;">User Documentation</span></a>
|
||||
</span><span> or with our </span>
|
||||
<a href="https://www.odoo.com/documentation">
|
||||
|
||||
@@ -406,7 +406,7 @@ class MergePartnerAutomatic(models.TransientModel):
|
||||
:param models : dict mapping a model name with its foreign key with res_partner table
|
||||
"""
|
||||
return any(
|
||||
self.env[model].search_count([(field, 'in', aggr_ids)], limit=1)
|
||||
self.env[model].search_count([(field, 'in', aggr_ids)])
|
||||
for model, field in models.iteritems()
|
||||
)
|
||||
|
||||
|
||||
@@ -51,8 +51,11 @@ class StockPicking(models.Model):
|
||||
_inherit = 'stock.picking'
|
||||
|
||||
def _default_uom(self):
|
||||
uom_categ_id = self.env.ref('product.product_uom_categ_kgm').id
|
||||
return self.env['product.uom'].search([('category_id', '=', uom_categ_id), ('factor', '=', 1)], limit=1)
|
||||
weight_uom_id = self.env.ref('product.product_uom_kgm', raise_if_not_found=False)
|
||||
if not weight_uom_id:
|
||||
uom_categ_id = self.env.ref('product.product_uom_categ_kgm').id
|
||||
weight_uom_id = self.env['product.uom'].search([('category_id', '=', uom_categ_id), ('factor', '=', 1)], limit=1)
|
||||
return weight_uom_id
|
||||
|
||||
@api.one
|
||||
@api.depends('pack_operation_ids')
|
||||
|
||||
@@ -36,7 +36,7 @@ class EventTicket(models.Model):
|
||||
name = fields.Char(string='Name', required=True, translate=True)
|
||||
event_id = fields.Many2one('event.event', string="Event", required=True, ondelete='cascade')
|
||||
product_id = fields.Many2one('product.product', string='Product',
|
||||
required=True, domain=["|", ("event_type_id", "!=", False), ("event_ok", "=", True)],
|
||||
required=True, domain=[("event_ok", "=", True)],
|
||||
default=_default_product_id)
|
||||
registration_ids = fields.One2many('event.registration', 'event_ticket_id', string='Registrations')
|
||||
price = fields.Float(string='Price', digits=dp.get_precision('Product Price'))
|
||||
@@ -44,6 +44,7 @@ class EventTicket(models.Model):
|
||||
is_expired = fields.Boolean(string='Is Expired', compute='_compute_is_expired')
|
||||
|
||||
price_reduce = fields.Float(string="Price Reduce", compute="_compute_price_reduce", digits=dp.get_precision('Product Price'))
|
||||
price_reduce_taxinc = fields.Float(compute='_get_price_reduce_tax', string='Price Reduce Tax inc')
|
||||
# seats fields
|
||||
seats_availability = fields.Selection([('limited', 'Limited'), ('unlimited', 'Unlimited')],
|
||||
string='Available Seat', required=True, store=True, compute='_compute_seats', default="limited")
|
||||
@@ -71,6 +72,13 @@ class EventTicket(models.Model):
|
||||
discount = product.lst_price and (product.lst_price - product.price) / product.lst_price or 0.0
|
||||
record.price_reduce = (1.0 - discount) * record.price
|
||||
|
||||
def _get_price_reduce_tax(self):
|
||||
for record in self:
|
||||
# sudo necessary here since the field is most probably accessed through the website
|
||||
tax_ids = record.sudo().product_id.taxes_id.filtered(lambda r: r.company_id == record.event_id.company_id)
|
||||
taxes = tax_ids.compute_all(record.price_reduce, record.event_id.company_id.currency_id, 1.0, product=record.product_id)
|
||||
record.price_reduce_taxinc = taxes['total_included']
|
||||
|
||||
@api.multi
|
||||
@api.depends('seats_max', 'registration_ids.state')
|
||||
def _compute_seats(self):
|
||||
|
||||
@@ -795,6 +795,7 @@ class GoogleCalendar(models.AbstractModel):
|
||||
actToDo = event.OP
|
||||
actSrc = event.OP.src
|
||||
|
||||
# To avoid redefining 'self', all method below should use 'recs' instead of 'self'
|
||||
recs = self.with_context(curr_attendee=event.OE.attendee_id)
|
||||
|
||||
if isinstance(actToDo, NothingToDo):
|
||||
@@ -828,12 +829,9 @@ class GoogleCalendar(models.AbstractModel):
|
||||
|
||||
if event_to_synchronize[base_event][0][1].OE.event_id:
|
||||
parent_event['id'] = "%s-%s" % (event_to_synchronize[base_event][0][1].OE.event_id, new_google_event_id)
|
||||
res = self.update_from_google(parent_event, event.GG.event, "copy")
|
||||
res = recs.update_from_google(parent_event, event.GG.event, "copy")
|
||||
else:
|
||||
self.create_from_google(event, my_partner_id)
|
||||
|
||||
parent_event['id'] = "%s-%s" % (event_to_synchronize[base_event][0][1].OE.event_id, new_google_event_id)
|
||||
res = recs.update_from_google(parent_event, event.GG.event, "copy")
|
||||
recs.create_from_google(event, my_partner_id)
|
||||
else:
|
||||
parent_oe_id = event_to_synchronize[base_event][0][1].OE.event_id
|
||||
if parent_oe_id:
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import api, models
|
||||
from openerp.exceptions import AccessError
|
||||
|
||||
|
||||
class Partner(models.Model):
|
||||
@@ -13,12 +14,10 @@ class Partner(models.Model):
|
||||
""" Extend the mail's static mention suggestions by adding the employees. """
|
||||
suggestions = super(Partner, self).get_static_mention_suggestions()
|
||||
|
||||
employee_group_id = self.env['ir.model.data'].xmlid_to_res_id('base.group_user')
|
||||
self._cr.execute("""
|
||||
SELECT P.id, P.name, P.email
|
||||
FROM res_users U
|
||||
INNER JOIN res_groups_users_rel R ON U.id = R.uid
|
||||
INNER JOIN res_partner P ON P.id = U.partner_id
|
||||
WHERE R.gid = %s AND U.active = 't'""", (employee_group_id,))
|
||||
suggestions.append(self._cr.dictfetchall())
|
||||
return suggestions
|
||||
try:
|
||||
employee_group = self.env.ref('base.group_user')
|
||||
for user in employee_group.users:
|
||||
suggestions.append((user.partner_id.id, user.name, user.email))
|
||||
return suggestions
|
||||
except AccessError:
|
||||
return suggestions
|
||||
|
||||
@@ -74,7 +74,7 @@ class HrEmployee(models.Model):
|
||||
@api.multi
|
||||
def attendance_manual(self, next_action, entered_pin=None):
|
||||
self.ensure_one()
|
||||
if self.env['res.users'].browse(SUPERUSER_ID).has_group('hr_attendance.group_hr_attendance_use_pin') and (self.user_id and self.user_id.id != self._uid or not self.user_id):
|
||||
if not (entered_pin is None) or self.env['res.users'].browse(SUPERUSER_ID).has_group('hr_attendance.group_hr_attendance_use_pin') and (self.user_id and self.user_id.id != self._uid or not self.user_id):
|
||||
if entered_pin != self.pin:
|
||||
return {'warning': _('Wrong PIN')}
|
||||
return self.attendance_action(next_action)
|
||||
|
||||
@@ -562,7 +562,7 @@
|
||||
<filter string="New Mail" name="message_unread" domain="[('message_unread', '=', True)]"/>
|
||||
<group expand="0" string="Group By">
|
||||
<filter string="Employee" domain="[]" context="{'group_by': 'employee_id'}"/>
|
||||
<filter string="Department" domain="[]" context="{'group_by': 'department_id'}" groups="hr.group_multi_departments"/>
|
||||
<filter string="Department" domain="[]" context="{'group_by': 'department_id'}"/>
|
||||
<filter string="Company" domain="[]" context="{'group_by': 'company_id'}" groups="base.group_multi_company"/>
|
||||
<filter string="Expenses Month" domain="[]" context="{'group_by': 'accounting_date'}" help="Expenses by Month"/>
|
||||
<filter string="State" domain="[]" context="{'group_by': 'state'}" name="state"/>
|
||||
|
||||
@@ -330,7 +330,7 @@ class Holidays(models.Model):
|
||||
def name_get(self):
|
||||
res = []
|
||||
for leave in self:
|
||||
res.append((leave.id, _("%s on %s : %.2f day(s)") % (leave.employee_id.name, leave.holiday_status_id.name, leave.number_of_days_temp)))
|
||||
res.append((leave.id, _("%s on %s : %.2f day(s)") % (leave.employee_id.name or leave.category_id.name, leave.holiday_status_id.name, leave.number_of_days_temp)))
|
||||
return res
|
||||
|
||||
def _check_state_access_right(self, vals):
|
||||
|
||||
@@ -27,7 +27,7 @@ class HrTimesheetSheet(models.Model):
|
||||
return (datetime.today() + relativedelta(weekday=0, days=-6)).strftime('%Y-%m-%d')
|
||||
elif r == 'year':
|
||||
return time.strftime('%Y-01-01')
|
||||
return fields.date.context_today(self)
|
||||
return fields.Date.context_today(self)
|
||||
|
||||
def _default_date_to(self):
|
||||
user = self.env['res.users'].browse(self.env.uid)
|
||||
@@ -38,7 +38,7 @@ class HrTimesheetSheet(models.Model):
|
||||
return (datetime.today() + relativedelta(weekday=6)).strftime('%Y-%m-%d')
|
||||
elif r == 'year':
|
||||
return time.strftime('%Y-12-31')
|
||||
return fields.date.context_today(self)
|
||||
return fields.Date.context_today(self)
|
||||
|
||||
def _default_employee(self):
|
||||
emp_ids = self.env['hr.employee'].search([('user_id', '=', self.env.uid)])
|
||||
|
||||
@@ -75,12 +75,13 @@ class MailController(http.Controller):
|
||||
return cls._redirect_to_messaging()
|
||||
|
||||
# the record has a window redirection: check access rights
|
||||
if not RecordModel.sudo(uid).check_access_rights('read', raise_exception=False):
|
||||
return cls._redirect_to_messaging()
|
||||
try:
|
||||
record_sudo.sudo(uid).check_access_rule('read')
|
||||
except AccessError:
|
||||
return cls._redirect_to_messaging()
|
||||
if uid is not None:
|
||||
if not RecordModel.sudo(uid).check_access_rights('read', raise_exception=False):
|
||||
return cls._redirect_to_messaging()
|
||||
try:
|
||||
record_sudo.sudo(uid).check_access_rule('read')
|
||||
except AccessError:
|
||||
return cls._redirect_to_messaging()
|
||||
|
||||
url_params = {
|
||||
'view_type': record_action['view_type'],
|
||||
|
||||
@@ -118,23 +118,31 @@ class Followers(models.Model):
|
||||
# Modifying followers change access rights to individual documents. As the
|
||||
# cache may contain accessible/inaccessible data, one has to refresh it.
|
||||
#
|
||||
@api.multi
|
||||
def _invalidate_documents(self):
|
||||
""" Invalidate the cache of the documents followed by ``self``. """
|
||||
for record in self:
|
||||
if record.res_id:
|
||||
self.env[record.res_model].invalidate_cache(ids=[record.res_id])
|
||||
|
||||
@api.model
|
||||
def create(self, vals):
|
||||
res = super(Followers, self).create(vals)
|
||||
self.invalidate_cache()
|
||||
res._invalidate_documents()
|
||||
return res
|
||||
|
||||
@api.multi
|
||||
def write(self, vals):
|
||||
if 'res_model' in vals or 'res_id' in vals:
|
||||
self._invalidate_documents()
|
||||
res = super(Followers, self).write(vals)
|
||||
self.invalidate_cache()
|
||||
self._invalidate_documents()
|
||||
return res
|
||||
|
||||
@api.multi
|
||||
def unlink(self):
|
||||
res = super(Followers, self).unlink()
|
||||
self.invalidate_cache()
|
||||
return res
|
||||
self._invalidate_documents()
|
||||
return super(Followers, self).unlink()
|
||||
|
||||
_sql_constraints = [
|
||||
('mail_followers_res_partner_res_model_id_uniq', 'unique(res_model,res_id,partner_id)', 'Error, a partner cannot follow twice the same object.'),
|
||||
|
||||
@@ -699,6 +699,13 @@ class Message(models.Model):
|
||||
message_id = tools.generate_tracking_message_id('private')
|
||||
return message_id
|
||||
|
||||
@api.multi
|
||||
def _invalidate_documents(self):
|
||||
""" Invalidate the cache of the documents followed by ``self``. """
|
||||
for record in self:
|
||||
if record.res_id:
|
||||
self.env[record.model].invalidate_cache(ids=[record.res_id])
|
||||
|
||||
@api.model
|
||||
def create(self, values):
|
||||
# coming from mail.js that does not have pid in its values
|
||||
@@ -715,6 +722,7 @@ class Message(models.Model):
|
||||
values['record_name'] = self._get_record_name(values)
|
||||
|
||||
message = super(Message, self).create(values)
|
||||
message._invalidate_documents()
|
||||
|
||||
if not self.env.context.get('message_create_from_mail_mail'):
|
||||
message._notify(force_send=self.env.context.get('mail_notify_force_send', True),
|
||||
@@ -728,6 +736,14 @@ class Message(models.Model):
|
||||
self.check_access_rule('read')
|
||||
return super(Message, self).read(fields=fields, load=load)
|
||||
|
||||
@api.multi
|
||||
def write(self, vals):
|
||||
if 'model' in vals or 'res_id' in vals:
|
||||
self._invalidate_documents()
|
||||
res = super(Message, self).write(vals)
|
||||
self._invalidate_documents()
|
||||
return res
|
||||
|
||||
@api.multi
|
||||
def unlink(self):
|
||||
# cascade-delete attachments that are directly attached to the message (should only happen
|
||||
@@ -736,6 +752,7 @@ class Message(models.Model):
|
||||
self.mapped('attachment_ids').filtered(
|
||||
lambda attach: attach.res_model == self._name and (attach.res_id in self.ids or attach.res_id == 0)
|
||||
).unlink()
|
||||
self._invalidate_documents()
|
||||
return super(Message, self).unlink()
|
||||
|
||||
#------------------------------------------------------
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
import babel
|
||||
import base64
|
||||
import copy
|
||||
import datetime
|
||||
@@ -18,6 +19,20 @@ from odoo.exceptions import UserError
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def format_date(env, date, pattern=False):
|
||||
date = datetime.datetime.strptime(date[:10], tools.DEFAULT_SERVER_DATE_FORMAT)
|
||||
lang_code = env.context.get('lang') or 'en_US'
|
||||
if not pattern:
|
||||
lang = env['res.lang']._lang_get(lang_code)
|
||||
pattern = lang.date_format
|
||||
try:
|
||||
locale = babel.Locale.parse(lang_code)
|
||||
pattern = tools.posix_to_ldml(pattern, locale=locale)
|
||||
return babel.dates.format_date(date, format=pattern, locale=locale)
|
||||
except babel.core.UnknownLocaleError:
|
||||
return date.strftime(pattern)
|
||||
|
||||
|
||||
def format_tz(env, dt, tz=False, format=False):
|
||||
record_user_timestamp = env.user.sudo().with_context(tz=tz or env.user.sudo().tz or 'UTC')
|
||||
timestamp = datetime.datetime.strptime(dt, tools.DEFAULT_SERVER_DATETIME_FORMAT)
|
||||
@@ -350,6 +365,7 @@ class MailTemplate(models.Model):
|
||||
for record in records:
|
||||
res_to_rec[record.id] = record
|
||||
variables = {
|
||||
'format_date': lambda date, format=False, context=self._context: format_date(self.env, date, format),
|
||||
'format_tz': lambda dt, tz=False, format=False, context=self._context: format_tz(self.env, dt, tz, format),
|
||||
'user': self.env.user,
|
||||
'ctx': self._context, # context kw would clash with mako internals
|
||||
|
||||
@@ -127,8 +127,12 @@ var ChatAction = Widget.extend(ControlPanelMixin, {
|
||||
this.$(".o_mail_annoying_notification_bar").slideUp();
|
||||
var def = window.Notification.requestPermission();
|
||||
if (def) {
|
||||
def.then(function () {
|
||||
utils.send_notification('Permission granted', 'Odoo has now the permission to send you native notifications on this device.');
|
||||
def.then(function (value) {
|
||||
if (value === 'denied') {
|
||||
utils.send_notification(_t('Permission denied'), _t('Odoo will not have the permission to send native notifications on this device.'));
|
||||
} else {
|
||||
utils.send_notification(_t('Permission granted'), _t('Odoo has now the permission to send you native notifications on this device.'));
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
@@ -130,7 +130,6 @@ var accented_letters_mapping = {
|
||||
'oe': 'œ',
|
||||
'u': '[ùúûűü]',
|
||||
'y': '[ýÿ]',
|
||||
' ': '[()\\[\\]]',
|
||||
};
|
||||
function unaccent (str) {
|
||||
_.each(accented_letters_mapping, function (value, key) {
|
||||
|
||||
@@ -12,6 +12,17 @@ class TestMailFeatures(TestMail):
|
||||
alias = self.env['mail.alias'].with_context(alias_model_name='mail.channel').create({'alias_name': 'b4r+_#_R3wl$$'})
|
||||
self.assertEqual(alias.alias_name, 'b4r+_-_r3wl-', 'Disallowed chars should be replaced by hyphens')
|
||||
|
||||
def test_10_cache_invalidation(self):
|
||||
""" Test that creating a mail-thread record does not invalidate the whole cache. """
|
||||
# make a new record in cache
|
||||
record = self.env['res.partner'].new({'name': 'Brave New Partner'})
|
||||
self.assertTrue(record.name)
|
||||
|
||||
# creating a mail-thread record should not invalidate the whole cache
|
||||
self.env['res.partner'].create({'name': 'Actual Partner'})
|
||||
self.assertTrue(record.name)
|
||||
|
||||
|
||||
@mute_logger('odoo.addons.mail.models.mail_mail')
|
||||
def test_needaction(self):
|
||||
na_emp1_base = self.group_pigs.sudo(self.user_employee).message_needaction_counter
|
||||
|
||||
@@ -268,3 +268,8 @@ class TestMailMessage(TestMail):
|
||||
msg.toggle_message_starred()
|
||||
self.assertFalse(msg.starred)
|
||||
self.assertTrue(msg_emp.starred)
|
||||
|
||||
def test_60_cache_invalidation(self):
|
||||
msg_cnt = len(self.group_pigs.message_ids)
|
||||
self.group_pigs.message_post(body='Hi!', subject='test')
|
||||
self.assertEqual(len(self.group_pigs.message_ids), msg_cnt + 1)
|
||||
|
||||
@@ -340,6 +340,16 @@ class MarketingCampaignActivity(models.Model):
|
||||
self.ensure_one()
|
||||
return self.email_template_id.send_mail(workitem.res_id)
|
||||
|
||||
@api.multi
|
||||
def _process_wi_report(self, workitem):
|
||||
self.ensure_one()
|
||||
return self.report_id.render_report(workitem.res_id, self.report_id.report_name, None)
|
||||
|
||||
@api.multi
|
||||
def _process_wi_action(self, workitem):
|
||||
self.ensure_one()
|
||||
return self.server_action_id.run()
|
||||
|
||||
@api.multi
|
||||
def process(self, workitem):
|
||||
self.ensure_one()
|
||||
|
||||
@@ -94,7 +94,7 @@ class MrpProduction(models.Model):
|
||||
help="Bill of Materials allow you to define the list of required raw materials to make a finished product.")
|
||||
routing_id = fields.Many2one(
|
||||
'mrp.routing', 'Routing',
|
||||
readonly=True, related='bom_id.routing_id', store=True,
|
||||
readonly=True, compute='_compute_routing', store=True,
|
||||
help="The list of operations (list of work centers) to produce the finished product. The routing "
|
||||
"is mainly used to compute work center costs during operations and to plan future loads on "
|
||||
"work centers based on production planning.")
|
||||
@@ -152,6 +152,15 @@ class MrpProduction(models.Model):
|
||||
priority = fields.Selection([('0', 'Not urgent'), ('1', 'Normal'), ('2', 'Urgent'), ('3', 'Very Urgent')], 'Priority',
|
||||
readonly=True, states={'confirmed': [('readonly', False)]}, default='1')
|
||||
|
||||
@api.multi
|
||||
@api.depends('bom_id.routing_id', 'bom_id.routing_id.operation_ids')
|
||||
def _compute_routing(self):
|
||||
for production in self:
|
||||
if production.bom_id.routing_id.operation_ids:
|
||||
production.routing_id = production.bom_id.routing_id.id
|
||||
else:
|
||||
production.routing_id = False
|
||||
|
||||
@api.multi
|
||||
@api.depends('workorder_ids')
|
||||
def _compute_workorder_count(self):
|
||||
@@ -243,10 +252,6 @@ class MrpProduction(models.Model):
|
||||
self.location_src_id = self.picking_type_id.default_location_src_id.id or location.id
|
||||
self.location_dest_id = self.picking_type_id.default_location_dest_id.id or location.id
|
||||
|
||||
@api.onchange('bom_id')
|
||||
def onchange_bom_id(self):
|
||||
self.routing_id = self.bom_id.routing_id.id
|
||||
|
||||
@api.model
|
||||
def create(self, values):
|
||||
if not values.get('name', False) or values['name'] == _('New'):
|
||||
@@ -345,7 +350,7 @@ class MrpProduction(models.Model):
|
||||
mto_route = False
|
||||
for move in self.move_raw_ids:
|
||||
product = move.product_id
|
||||
routes = product.route_ids + product.categ_id.route_ids
|
||||
routes = product.route_ids + product.route_from_categ_ids
|
||||
# TODO: optimize with read_group?
|
||||
pull = self.env['procurement.rule'].search([('route_id', 'in', [x.id for x in routes]), ('location_src_id', '=', move.location_id.id),
|
||||
('location_id', '=', move.location_dest_id.id)], limit=1)
|
||||
@@ -481,11 +486,10 @@ class MrpProduction(models.Model):
|
||||
@api.multi
|
||||
def post_inventory(self):
|
||||
for order in self:
|
||||
moves_to_do = order.move_raw_ids
|
||||
moves_to_do = order.move_raw_ids.filtered(lambda x: x.state not in ('done', 'cancel'))
|
||||
moves_to_do.action_done()
|
||||
#order.move_finished_ids.filtered(lambda x: x.state not in ('done','cancel')).move_validate()
|
||||
order._cal_price(moves_to_do)
|
||||
moves_to_finish = order.move_finished_ids
|
||||
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
|
||||
|
||||
@@ -377,8 +377,6 @@ class MrpWorkorder(models.Model):
|
||||
self.ensure_one()
|
||||
self.end_all()
|
||||
self.write({'state': 'done', 'date_finished': fields.Datetime.now()})
|
||||
if not self.production_id.workorder_ids.filtered(lambda x: x.state not in ('done','cancel')):
|
||||
self.production_id.post_inventory() # User should put it to done manually
|
||||
|
||||
@api.multi
|
||||
def end_previous(self, doall=False):
|
||||
|
||||
@@ -16,12 +16,15 @@ class StockPickingType(models.Model):
|
||||
mrp_picking_types = self.filtered(lambda picking: picking.code == 'mrp_operation')
|
||||
if not mrp_picking_types:
|
||||
return
|
||||
|
||||
MrpProduction = self.env['mrp.production']
|
||||
count_mo_waiting = MrpProduction.search_count([('availability', '=', 'waiting')])
|
||||
count_mo_todo = MrpProduction.search_count([('state', 'in', ('confirmed', 'planned', 'progress'))])
|
||||
count_mo_late = MrpProduction.search_count(['&', ('date_planned_start', '<', fields.Date.today()), ('state', '=', 'confirmed')])
|
||||
for picking in mrp_picking_types:
|
||||
picking.count_mo_waiting = count_mo_waiting
|
||||
picking.count_mo_todo = count_mo_todo
|
||||
picking.count_mo_late = count_mo_late
|
||||
domains = {
|
||||
'count_mo_waiting': [('availability', '=', 'waiting')],
|
||||
'count_mo_todo': [('state', 'in', ('confirmed', 'planned', 'progress'))],
|
||||
'count_mo_late': [('date_planned_start', '<', fields.Date.today()), ('state', '=', 'confirmed')],
|
||||
}
|
||||
for field in domains:
|
||||
data = self.env['mrp.production'].read_group(domains[field] +
|
||||
[('state', 'not in', ('done', 'cancel')), ('picking_type_id', 'in', self.ids)],
|
||||
['picking_type_id'], ['picking_type_id'])
|
||||
count = dict(map(lambda x: (x['picking_type_id'] and x['picking_type_id'][0], x['picking_type_id_count']), data))
|
||||
for record in mrp_picking_types:
|
||||
record[field] = count.get(record.id, 0)
|
||||
@@ -332,3 +332,98 @@ class TestMrpOrder(TestMrpCommon):
|
||||
production_2.action_assign()
|
||||
# check sub product availability state is assigned
|
||||
self.assertEqual(production_2.availability, 'assigned', 'Production order should be availability for assigned state')
|
||||
|
||||
def test_empty_routing(self):
|
||||
""" Check what happens when you work with an empty routing"""
|
||||
routing = self.env['mrp.routing'].create({'name': 'Routing without operations',
|
||||
'location_id': self.warehouse_1.wh_input_stock_loc_id.id,})
|
||||
self.bom_3.routing_id = routing.id
|
||||
production = self.env['mrp.production'].create({'name': 'MO test',
|
||||
'product_id': self.product_6.id,
|
||||
'product_qty': 3,
|
||||
'bom_id': self.bom_3.id,
|
||||
'product_uom_id': self.product_6.uom_id.id,})
|
||||
self.assertEqual(production.routing_id.id, False, 'The routing field should be empty on the mo')
|
||||
self.assertEqual(production.move_raw_ids[0].location_id.id, self.warehouse_1.wh_input_stock_loc_id.id, 'Raw moves start location should have altered.')
|
||||
|
||||
def test_multiple_post_inventory(self):
|
||||
""" Check the consumed quants of the produced quants when intermediate calls to `post_inventory` during a MO."""
|
||||
|
||||
# create a bom for `custom_laptop` with components that aren't tracked
|
||||
unit = self.ref("product.product_uom_unit")
|
||||
custom_laptop = self.env.ref("product.product_product_27")
|
||||
custom_laptop.tracking = 'none'
|
||||
product_charger = self.env['product.product'].create({
|
||||
'name': 'Charger',
|
||||
'type': 'product',
|
||||
'uom_id': unit,
|
||||
'uom_po_id': unit})
|
||||
product_keybord = self.env['product.product'].create({
|
||||
'name': 'Usb Keybord',
|
||||
'type': 'product',
|
||||
'uom_id': unit,
|
||||
'uom_po_id': unit})
|
||||
bom_custom_laptop = self.env['mrp.bom'].create({
|
||||
'product_tmpl_id': custom_laptop.product_tmpl_id.id,
|
||||
'product_qty': 1,
|
||||
'product_uom_id': unit,
|
||||
'bom_line_ids': [(0, 0, {
|
||||
'product_id': product_charger.id,
|
||||
'product_qty': 1,
|
||||
'product_uom_id': unit
|
||||
}), (0, 0, {
|
||||
'product_id': product_keybord.id,
|
||||
'product_qty': 1,
|
||||
'product_uom_id': unit
|
||||
})]
|
||||
})
|
||||
|
||||
# put the needed products in stock
|
||||
source_location_id = self.ref('stock.stock_location_14')
|
||||
inventory = self.env['stock.inventory'].create({
|
||||
'name': 'Inventory Product Table',
|
||||
'filter': 'partial',
|
||||
'line_ids': [(0, 0, {
|
||||
'product_id': product_charger.id,
|
||||
'product_uom_id': product_charger.uom_id.id,
|
||||
'product_qty': 2,
|
||||
'location_id': source_location_id
|
||||
}), (0, 0, {
|
||||
'product_id': product_keybord.id,
|
||||
'product_uom_id': product_keybord.uom_id.id,
|
||||
'product_qty': 2,
|
||||
'location_id': source_location_id
|
||||
})]
|
||||
})
|
||||
inventory.action_done()
|
||||
|
||||
# create a mo for this bom
|
||||
mo_custom_laptop = self.env['mrp.production'].create({
|
||||
'product_id': custom_laptop.id,
|
||||
'product_qty': 2,
|
||||
'product_uom_id': unit,
|
||||
'bom_id': bom_custom_laptop.id
|
||||
})
|
||||
mo_custom_laptop.action_assign()
|
||||
self.assertEqual(mo_custom_laptop.availability, 'assigned')
|
||||
|
||||
# produce one item, call `post_inventory`
|
||||
context = {"active_ids": [mo_custom_laptop.id], "active_id": mo_custom_laptop.id}
|
||||
custom_laptop_produce = self.env['mrp.product.produce'].with_context(context).create({'product_qty': 1.00})
|
||||
custom_laptop_produce.do_produce()
|
||||
mo_custom_laptop.post_inventory()
|
||||
|
||||
# check the consumed quants of the produced quant
|
||||
first_move = mo_custom_laptop.move_finished_ids.filtered(lambda mo: mo.state == 'done')
|
||||
self.assertEquals(sum(first_move.quant_ids.mapped('consumed_quant_ids').mapped('qty')), 2)
|
||||
|
||||
second_move = mo_custom_laptop.move_finished_ids.filtered(lambda mo: mo.state == 'confirmed')
|
||||
|
||||
# produce the second item, call `post_inventory`
|
||||
context = {"active_ids": [mo_custom_laptop.id], "active_id": mo_custom_laptop.id}
|
||||
custom_laptop_produce = self.env['mrp.product.produce'].with_context(context).create({'product_qty': 1.00})
|
||||
custom_laptop_produce.do_produce()
|
||||
mo_custom_laptop.post_inventory()
|
||||
|
||||
# check the consumed quants of the newly produced quant
|
||||
self.assertEquals(sum(second_move.quant_ids.mapped('consumed_quant_ids').mapped('qty')), 2)
|
||||
|
||||
@@ -105,3 +105,38 @@ class TestProcurement(TestMrpCommon):
|
||||
production_product_6.button_mark_done()
|
||||
self.assertEqual(production_product_6.state, 'done', 'Production order should be in state done')
|
||||
self.assertEqual(self.product_6.qty_available, 24, 'Wrong quantity available of finished product.')
|
||||
|
||||
def test_procurement_2(self):
|
||||
"""Check that a manufacturing order create the right procurements when the route are set on
|
||||
a parent category of a product"""
|
||||
# find a child category id
|
||||
all_categ_id = self.env['product.category'].search([('parent_id', '=', None)], limit=1)
|
||||
child_categ_id = self.env['product.category'].search([('parent_id', '=', all_categ_id.id)], limit=1)
|
||||
|
||||
# set the product of `self.bom_1` to this child category
|
||||
for bom_line_id in self.bom_1.bom_line_ids:
|
||||
# check that no routes are defined on the product
|
||||
self.assertEquals(len(bom_line_id.product_id.route_ids), 0)
|
||||
# set the category of the product to a child category
|
||||
bom_line_id.product_id.categ_id = child_categ_id
|
||||
|
||||
# set the MTO route to the parent category (all)
|
||||
self.warehouse = self.env.ref('stock.warehouse0')
|
||||
mto_route = self.warehouse.mto_pull_id.route_id
|
||||
mto_route.product_categ_selectable = True
|
||||
all_categ_id.write({'route_ids': [(6, 0, [mto_route.id])]})
|
||||
|
||||
# create MO
|
||||
production_product_4 = self.env['mrp.production'].create({
|
||||
'name': 'MO/Test-00002',
|
||||
'product_id': self.product_4.id,
|
||||
'product_qty': 1,
|
||||
'bom_id': self.bom_1.id,
|
||||
'product_uom_id': self.product_4.uom_id.id,
|
||||
})
|
||||
|
||||
# check that procurement are correctly created
|
||||
procurement = self.env['procurement.order'].search(
|
||||
[('group_id', '=', production_product_4.procurement_group_id.id),
|
||||
('product_id', 'in', self.bom_1.bom_line_ids.mapped('product_id.id'))])
|
||||
self.assertEqual(len(procurement), 2)
|
||||
|
||||
@@ -123,9 +123,10 @@ class TestWorkOrderProcess(common.TransactionCase):
|
||||
# This behaviour was changed
|
||||
#self.assertEqual(production_table.state, 'done', "Production order should be in done state.")
|
||||
|
||||
# ----------------------------------------
|
||||
# Check consume quants and produce quants.
|
||||
# ----------------------------------------
|
||||
# ---------------------------------------------------------------
|
||||
# Check consume quants and produce quants after posting inventory
|
||||
# ---------------------------------------------------------------
|
||||
production_table.button_mark_done()
|
||||
self.assertEqual(sum(move_table_sheet.quant_ids.mapped('qty')), 1, "Wrong quantity of consumed product %s" % move_table_sheet.product_id.name)
|
||||
self.assertEqual(sum(move_leg.quant_ids.mapped('qty')), 4, "Wrong quantity of consumed product %s" % move_leg.product_id.name)
|
||||
self.assertEqual(sum(move_table_bolt.quant_ids.mapped('qty')), 4, "Wrong quantity of consumed product %s" % move_table_bolt.product_id.name)
|
||||
|
||||
@@ -320,7 +320,8 @@
|
||||
<field name="view_mode">tree,form</field>
|
||||
<field name="view_id" eval="False"/>
|
||||
<field name="search_view_id" ref="view_mrp_production_filter"/>
|
||||
<field name="context">{}</field>
|
||||
<field name="domain">[('picking_type_id', '=', active_id)]</field>
|
||||
<field name="context">{'default_picking_type_id': active_id}</field>
|
||||
</record>
|
||||
|
||||
|
||||
|
||||
@@ -17,7 +17,8 @@ class AccountPayment(models.Model):
|
||||
def _onchange_partner_id(self):
|
||||
res = {}
|
||||
if self.partner_id:
|
||||
res['domain'] = {'payment_token_id': [('partner_id', '=', self.partner_id.id), ('acquirer_id.auto_confirm', '!=', 'authorize')]}
|
||||
partners = self.partner_id | self.partner_id.commercial_partner_id | self.partner_id.commercial_partner_id.child_ids
|
||||
res['domain'] = {'payment_token_id': [('partner_id', 'in', partners.ids), ('acquirer_id.auto_confirm', '!=', 'authorize')]}
|
||||
|
||||
return res
|
||||
|
||||
@@ -57,4 +58,4 @@ class AccountPayment(models.Model):
|
||||
if not s2s_result or tx.state != 'done':
|
||||
raise ValidationError(_("Payment transaction failed (%s)") % tx.state_message)
|
||||
|
||||
self.payment_transaction_id = tx
|
||||
self.payment_transaction_id = tx
|
||||
|
||||
@@ -433,7 +433,7 @@ class PaymentTransaction(models.Model):
|
||||
# duplicate partner / transaction data to store the values at transaction time
|
||||
partner_id = fields.Many2one('res.partner', 'Partner', track_visibility='onchange')
|
||||
partner_name = fields.Char('Partner Name')
|
||||
partner_lang = fields.Selection(_lang_get, 'Language', default='en_US')
|
||||
partner_lang = fields.Selection(_lang_get, 'Language', default=lambda self: self.env.lang)
|
||||
partner_email = fields.Char('Email')
|
||||
partner_zip = fields.Char('Zip')
|
||||
partner_address = fields.Char('Address')
|
||||
@@ -634,7 +634,7 @@ class PaymentTransaction(models.Model):
|
||||
|
||||
class PaymentToken(models.Model):
|
||||
_name = 'payment.token'
|
||||
_order = 'partner_id'
|
||||
_order = 'partner_id, id desc'
|
||||
|
||||
name = fields.Char('Name', help='Name of the payment token')
|
||||
short_name = fields.Char('Short name', compute='_compute_short_name')
|
||||
|
||||
@@ -32,7 +32,7 @@ class StripeController(http.Controller):
|
||||
tx = request.env['payment.transaction'].sudo().browse(
|
||||
int(request.session.get('sale_transaction_id') or request.session.get('website_payment_tx_id', False))
|
||||
)
|
||||
response = tx._create_stripe_charge(tokenid=post['tokenid'])
|
||||
response = tx._create_stripe_charge(tokenid=post['tokenid'], email=post['email'])
|
||||
_logger.info('Stripe: entering form_feedback with post data %s', pprint.pformat(response))
|
||||
if response:
|
||||
request.env['payment.transaction'].sudo().form_feedback(response, 'stripe')
|
||||
|
||||
@@ -81,7 +81,7 @@ class PaymentAcquirerStripe(models.Model):
|
||||
class PaymentTransactionStripe(models.Model):
|
||||
_inherit = 'payment.transaction'
|
||||
|
||||
def _create_stripe_charge(self, acquirer_ref=None, tokenid=None):
|
||||
def _create_stripe_charge(self, acquirer_ref=None, tokenid=None, email=None):
|
||||
api_url_charge = 'https://%s/charges' % (self.acquirer_id._get_stripe_api_url())
|
||||
charge_params = {
|
||||
'amount': int(self.amount*100), # Stripe takes amount in cents (https://support.stripe.com/questions/which-zero-decimal-currencies-does-stripe-support)
|
||||
@@ -92,6 +92,8 @@ class PaymentTransactionStripe(models.Model):
|
||||
charge_params['customer'] = acquirer_ref
|
||||
if tokenid:
|
||||
charge_params['card'] = str(tokenid)
|
||||
if email:
|
||||
charge_params['receipt_email'] = email
|
||||
r = requests.post(api_url_charge,
|
||||
auth=(self.acquirer_id.stripe_secret_key, ''),
|
||||
params=charge_params,
|
||||
|
||||
@@ -511,6 +511,7 @@ class PosOrder(models.Model):
|
||||
if to_invoice:
|
||||
pos_order.action_pos_order_invoice()
|
||||
pos_order.invoice_id.sudo().action_invoice_open()
|
||||
pos_order.account_move = pos_order.invoice_id.move_id
|
||||
return order_ids
|
||||
|
||||
def test_paid(self):
|
||||
|
||||
@@ -22,10 +22,11 @@ class PosSession(models.Model):
|
||||
orders = session.order_ids.filtered(lambda order: order.state == 'paid')
|
||||
journal_id = self.env['ir.config_parameter'].sudo().get_param(
|
||||
'pos.closing.journal_id_%s' % company_id, default=session.config_id.journal_id.id)
|
||||
|
||||
move = self.env['pos.order'].with_context(force_company=company_id)._create_account_move(session.start_at, session.name, int(journal_id), company_id)
|
||||
orders.with_context(force_company=company_id)._create_account_move_line(session, move)
|
||||
for order in session.order_ids.filtered(lambda o: o.state != 'done'):
|
||||
if order.state not in ('paid', 'invoiced'):
|
||||
for order in session.order_ids.filtered(lambda o: o.state not in ['done', 'invoiced']):
|
||||
if order.state not in ('paid'):
|
||||
raise UserError(_("You cannot confirm all orders of this session, because they don't have the 'paid' status"))
|
||||
order.action_pos_order_done()
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
<xpath expr="//button[@name='toggle_active']" position="before">
|
||||
<button class="oe_stat_button" name="%(procurement.procurement_order_action_product_template_stat)d"
|
||||
icon="fa-truck" string="Procurements" type="action"
|
||||
groups="stock.group_adv_location"
|
||||
groups="base.group_no_one"
|
||||
attrs="{'invisible':[('type', '=', 'service')]}"/>
|
||||
</xpath>
|
||||
</field>
|
||||
@@ -24,7 +24,7 @@
|
||||
<xpath expr="//button[@name='toggle_active']" position="before">
|
||||
<button class="oe_stat_button" name="%(procurement.procurement_order_action_product_product_stat)d"
|
||||
icon="fa-truck" string="Procurements" type="action"
|
||||
groups="stock.group_adv_location"
|
||||
groups="base.group_no_one"
|
||||
attrs="{'invisible':[('type', '=', 'service')]}"/>
|
||||
</xpath>
|
||||
</field>
|
||||
|
||||
@@ -552,7 +552,7 @@ class ProductProduct(models.Model):
|
||||
PriceHistory.create({
|
||||
'product_id': product.id,
|
||||
'cost': value,
|
||||
'company_id': self._context.get('force_compay', self.env.user.company_id.id),
|
||||
'company_id': self._context.get('force_company', self.env.user.company_id.id),
|
||||
})
|
||||
|
||||
@api.multi
|
||||
|
||||
@@ -31,7 +31,8 @@ class Partner(models.Model):
|
||||
|
||||
# update at each change country, and so erase old pricelist
|
||||
if self.property_product_pricelist or (actual and default_for_country and default_for_country.id != actual.id):
|
||||
self.env['ir.property'].sudo().set_multi(
|
||||
# keep the company of the current user before sudo
|
||||
self.env['ir.property'].with_context(force_company=self.env.user.company_id.id).sudo().set_multi(
|
||||
'property_product_pricelist',
|
||||
self._name,
|
||||
{self.id: self.property_product_pricelist or default_for_country.id},
|
||||
|
||||
@@ -44,7 +44,10 @@ class StockProductionLot(models.Model):
|
||||
# Assign dates according to products data
|
||||
@api.model
|
||||
def create(self, vals):
|
||||
vals.update(self._get_dates(vals.get('product_id')))
|
||||
dates = self._get_dates(vals.get('product_id'))
|
||||
for d in dates.keys():
|
||||
if not vals.get(d):
|
||||
vals[d] = dates[d]
|
||||
return super(StockProductionLot, self).create(vals)
|
||||
|
||||
@api.onchange('product_id')
|
||||
|
||||
@@ -36,11 +36,11 @@ class ProductProduct(models.Model):
|
||||
def _calc_price(self, bom):
|
||||
price = 0.0
|
||||
workcenter_cost = 0.0
|
||||
for sbom in bom.bom_line_ids:
|
||||
my_qty = sbom.product_qty
|
||||
result, result2 = bom.explode(self, 1)
|
||||
for sbom, sbom_data in result2:
|
||||
if not sbom.attribute_value_ids:
|
||||
# No attribute_value_ids means the bom line is not variant specific
|
||||
price += sbom.product_id.uom_id._compute_price(sbom.product_id.standard_price, sbom.product_uom_id) * my_qty
|
||||
price += sbom.product_id.uom_id._compute_price(sbom.product_id.standard_price, sbom.product_uom_id) * sbom_data['qty']
|
||||
if bom.routing_id:
|
||||
total_cost = 0.0
|
||||
for order in bom.routing_id.operation_ids:
|
||||
@@ -49,5 +49,5 @@ class ProductProduct(models.Model):
|
||||
price += bom.product_uom_id._compute_price(workcenter_cost, bom.product_id.uom_id)
|
||||
# Convert on product UoM quantities
|
||||
if price > 0:
|
||||
price = bom.product_uom_id._compute_price(price / bom.product_qty, bom.product_id.uom_id)
|
||||
price = bom.product_uom_id._compute_price(price / bom.product_qty, self.uom_id)
|
||||
return price
|
||||
|
||||
@@ -93,11 +93,10 @@
|
||||
<span style="font-style:italic;">You’re not alone</span>
|
||||
</p>
|
||||
<p dir="ltr" style="font-size:13px;margin-top:0pt;margin-bottom:8pt;">
|
||||
<span>We would be delighted to assist you along the way. Contact us at
|
||||
<a href="mailto:help@odoo.com"><span style="text-decoration:underline;">
|
||||
help@odoo.com</span></a> if you have any question. You can also discover
|
||||
how to get the best out of Odoo Project with our </span>
|
||||
<a href="https://www.odoo.com/documentation/user/9.0/project.html">
|
||||
<span>We would be delighted to assist you along the way. Contact us through our
|
||||
<a href="https://www.odoo.com/help">support form</a> if you have any question.
|
||||
You can also discover how to get the best out of Odoo Project with our </span>
|
||||
<a href="https://www.odoo.com/documentation/user/10.0/project.html">
|
||||
<span style="text-decoration:underline;">User Documentation</span></a>
|
||||
</span><span> or with our </span>
|
||||
<a href="https://www.odoo.com/documentation">
|
||||
|
||||
@@ -238,12 +238,10 @@
|
||||
<kanban class="oe_background_grey o_kanban_dashboard o_project_kanban" on_create="project.open_create_project">
|
||||
<field name="name"/>
|
||||
<field name="use_tasks"/>
|
||||
<field name="user_id"/>
|
||||
<field name="color"/>
|
||||
<field name="task_count"/>
|
||||
<field name="task_needaction_count"/>
|
||||
<field name="label_tasks"/>
|
||||
<field name="task_ids"/>
|
||||
<field name="alias_id"/>
|
||||
<field name="alias_name"/>
|
||||
<field name="alias_domain"/>
|
||||
|
||||
@@ -41,7 +41,7 @@ class ProjectIssueReport(models.Model):
|
||||
c.working_hours_open,
|
||||
c.working_hours_close,
|
||||
c.stage_id,
|
||||
date(c.date_closed) as date_closed,
|
||||
c.date_closed as date_closed,
|
||||
c.company_id as company_id,
|
||||
c.priority as priority,
|
||||
c.project_id as project_id,
|
||||
|
||||
@@ -38,12 +38,12 @@ class AccountInvoice(models.Model):
|
||||
if float_compare(qty, 0.0, precision_rounding=line.product_uom.rounding) <= 0:
|
||||
qty = 0.0
|
||||
taxes = line.taxes_id
|
||||
invoice_line_tax_ids = self.purchase_id.fiscal_position_id.map_tax(taxes)
|
||||
invoice_line_tax_ids = line.order_id.fiscal_position_id.map_tax(taxes)
|
||||
invoice_line = self.env['account.invoice.line']
|
||||
data = {
|
||||
'purchase_line_id': line.id,
|
||||
'name': self.purchase_id.name+': '+line.name,
|
||||
'origin': self.purchase_id.origin,
|
||||
'name': line.order_id.name+': '+line.name,
|
||||
'origin': line.order_id.origin,
|
||||
'uom_id': line.product_uom.id,
|
||||
'product_id': line.product_id.id,
|
||||
'account_id': invoice_line.with_context({'journal_id': self.journal_id.id, 'type': 'in_invoice'})._default_account(),
|
||||
@@ -54,7 +54,7 @@ class AccountInvoice(models.Model):
|
||||
'analytic_tag_ids': line.analytic_tag_ids.ids,
|
||||
'invoice_line_tax_ids': invoice_line_tax_ids.ids
|
||||
}
|
||||
account = invoice_line.get_invoice_line_account('in_invoice', line.product_id, self.purchase_id.fiscal_position_id, self.env.user.company_id)
|
||||
account = invoice_line.get_invoice_line_account('in_invoice', line.product_id, line.order_id.fiscal_position_id, self.env.user.company_id)
|
||||
if account:
|
||||
data['account_id'] = account.id
|
||||
return data
|
||||
|
||||
@@ -640,7 +640,7 @@ class PurchaseOrderLine(models.Model):
|
||||
sum_existing_moves = sum(x.product_qty for x in procurement.move_ids if x.state != 'cancel')
|
||||
existing_proc_qty = procurement.product_id.uom_id._compute_quantity(sum_existing_moves, procurement.product_uom)
|
||||
procurement_qty = procurement.product_uom._compute_quantity(procurement.product_qty, line.product_uom) - existing_proc_qty
|
||||
if float_compare(procurement_qty, 0.0, precision_rounding=procurement.product_uom.rounding) > 0:
|
||||
if float_compare(procurement_qty, 0.0, precision_rounding=procurement.product_uom.rounding) > 0 and float_compare(diff_quantity, 0.0, precision_rounding=line.product_uom.rounding) > 0:
|
||||
tmp = template.copy()
|
||||
tmp.update({
|
||||
'product_uom_qty': min(procurement_qty, diff_quantity),
|
||||
@@ -865,7 +865,7 @@ class ProcurementOrder(models.Model):
|
||||
"""Return the datetime value to use as Order Date (``date_order``) for the
|
||||
Purchase Order created to satisfy the given procurement. """
|
||||
self.ensure_one()
|
||||
seller_delay = int(self.product_id._select_seller().delay)
|
||||
seller_delay = int(self.product_id._select_seller(quantity=self.product_qty, uom_id=self.product_uom).delay)
|
||||
return schedule_date - relativedelta(days=seller_delay)
|
||||
|
||||
@api.multi
|
||||
|
||||
@@ -99,7 +99,6 @@ class Report(models.Model):
|
||||
|
||||
context = dict(self.env.context, inherit_branding=True) # Tell QWeb to brand the generated html
|
||||
|
||||
view_obj = self.env['ir.ui.view']
|
||||
# Browse the user instead of using the sudo self.env.user
|
||||
user = self.env['res.users'].browse(self.env.uid)
|
||||
website = None
|
||||
@@ -108,6 +107,7 @@ class Report(models.Model):
|
||||
website = request.website
|
||||
context = dict(context, translatable=context.get('lang') != request.website.default_lang_code)
|
||||
|
||||
view_obj = self.env['ir.ui.view'].with_context(context)
|
||||
values.update(
|
||||
time=time,
|
||||
context_timestamp=lambda t: fields.Datetime.context_timestamp(self.with_context(tz=user.tz), t),
|
||||
|
||||
@@ -683,6 +683,19 @@
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record model="ir.ui.view" id="account_invoice_line_tree">
|
||||
<field name="name">account.invoice.line.tree</field>
|
||||
<field name="model">account.invoice.line</field>
|
||||
<field name="inherit_id" ref="account.view_invoice_line_tree"/>
|
||||
<field name="arch" type="xml">
|
||||
<data>
|
||||
<xpath expr="//field[@name='discount']" position="attributes">
|
||||
<attribute name="groups">sale.group_discount_per_so_line</attribute>
|
||||
</xpath>
|
||||
</data>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record model="ir.ui.view" id="account_invoice_line_form">
|
||||
<field name="name">account.invoice.line.form</field>
|
||||
<field name="model">account.invoice.line</field>
|
||||
@@ -692,6 +705,22 @@
|
||||
<xpath expr="//group/group/field[@name='product_id']" position="after">
|
||||
<field name="layout_category_id" groups="sale.group_sale_layout"/>
|
||||
</xpath>
|
||||
<xpath expr="//field[@name='discount']" position="attributes">
|
||||
<attribute name="groups">sale.group_discount_per_so_line</attribute>
|
||||
</xpath>
|
||||
</data>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record model="ir.ui.view" id="account_invoice_supplier_form">
|
||||
<field name="name">account.invoice.supplier.form</field>
|
||||
<field name="model">account.invoice</field>
|
||||
<field name="inherit_id" ref="account.invoice_supplier_form"/>
|
||||
<field name="arch" type="xml">
|
||||
<data>
|
||||
<xpath expr="//field[@name='discount']" position="attributes">
|
||||
<attribute name="groups">sale.group_discount_per_so_line</attribute>
|
||||
</xpath>
|
||||
</data>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
@@ -165,8 +165,8 @@
|
||||
</group>
|
||||
<group name="email_template_and_project" position="before">
|
||||
<group name="sale_condition" string="Sale Conditions">
|
||||
<label for="warranty" groups="stock.group_stock_production_lot"/>
|
||||
<div groups="stock.group_stock_production_lot">
|
||||
<label for="warranty" groups="stock.group_production_lot"/>
|
||||
<div groups="stock.group_production_lot">
|
||||
<field name="warranty" class="oe_inline"/> months
|
||||
</div>
|
||||
<label for="sale_delay"/>
|
||||
@@ -178,6 +178,29 @@
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="product_template_view_form_procurement_button" model="ir.ui.view">
|
||||
<field name="name">product.template.stock.stat.button.group</field>
|
||||
<field name="model">product.template</field>
|
||||
<field name="inherit_id" ref="procurement.product_template_view_form_procurement_button"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//button[@name='%(procurement.procurement_order_action_product_template_stat)d']" position="attributes">
|
||||
<attribute name="groups">base.group_no_one,stock.group_adv_location</attribute>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="product_product_view_form_procurement_button" model="ir.ui.view">
|
||||
<field name="name">product.product.stock.stat.button.group</field>
|
||||
<field name="model">product.product</field>
|
||||
<field name="inherit_id" ref="procurement.product_product_view_form_procurement_button"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//button[@name='%(procurement.procurement_order_action_product_product_stat)d']" position="attributes">
|
||||
<attribute name="groups">base.group_no_one,stock.group_adv_location</attribute>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
|
||||
<record model="ir.ui.view" id="product_template_kanban_stock_view">
|
||||
<field name="name">Product Template Kanban Stock</field>
|
||||
<field name="model">product.template</field>
|
||||
|
||||
@@ -94,7 +94,7 @@
|
||||
<kanban class="o_kanban_mobile">
|
||||
<field name="product_id" domain="[('type','=','product')]"/>
|
||||
<field name="product_uom_id" groups="product.group_uom" string="UoM"/>
|
||||
<field name="location_id" domain="[('id', 'child_of', parent.location_id)]" groups="stock.group_locations"/>
|
||||
<field name="location_id" domain="[('id', 'child_of', parent.location_id)]" groups="stock.group_stock_multi_locations"/>
|
||||
<field name="prod_lot_id" domain="[('product_id', '=', product_id)]" context="{'default_product_id': product_id}" groups="stock.group_production_lot"/>
|
||||
<field name="package_id" domain="['|', ('location_id','=', False), ('location_id', '=', location_id)]"/>
|
||||
<field name="partner_id"/>
|
||||
|
||||
@@ -279,8 +279,8 @@
|
||||
<field name="owner_id" groups="stock.group_tracking_owner"/>
|
||||
<field name="location_id" domain="[('id', 'child_of', parent.location_id)]" invisible="1"/>
|
||||
<field name="location_dest_id" domain="[('id', 'child_of', parent.location_dest_id)]" invisible="1"/>
|
||||
<field name="from_loc" groups="stock.group_locations,stock.group_tracking_lot"/>
|
||||
<field name="to_loc" groups="stock.group_locations,stock.group_tracking_lot"/>
|
||||
<field name="from_loc" groups="stock.group_stock_multi_locations,stock.group_tracking_lot"/>
|
||||
<field name="to_loc" groups="stock.group_stock_multi_locations,stock.group_tracking_lot"/>
|
||||
<field name="result_package_id" groups="stock.group_tracking_lot" context="{'default_location_id': location_dest_id}" invisible="1"/>
|
||||
<field name="state" invisible="1"/>
|
||||
<field name="product_qty" readonly="1" attrs="{'required': [('product_id', '!=', False)]}"/>
|
||||
|
||||
@@ -47,7 +47,7 @@
|
||||
<label for="scrap_qty"/>
|
||||
<div class="o_row">
|
||||
<field name="scrap_qty"/>
|
||||
<field name="product_uom_id" groups="stock.product_group_uom"/>
|
||||
<field name="product_uom_id" groups="product.group_uom"/>
|
||||
</div>
|
||||
</group>
|
||||
<group>
|
||||
@@ -76,7 +76,7 @@
|
||||
<field name="create_date" string="Create Date"/>
|
||||
<field name="product_id"/>
|
||||
<field name="scrap_qty"/>
|
||||
<field name="product_uom_id" groups="stock.product_group_uom"/>
|
||||
<field name="product_uom_id" groups="product.group_uom"/>
|
||||
<field name="location_id" groups="stock.group_stock_multi_locations"/>
|
||||
<field name="scrap_location_id" groups="stock.group_stock_multi_locations"/>
|
||||
<field name="state"/>
|
||||
@@ -111,7 +111,7 @@
|
||||
<label for="scrap_qty"/>
|
||||
<div class="o_row">
|
||||
<field name="scrap_qty"/>
|
||||
<field name="product_uom_id" groups="stock.product_group_uom"/>
|
||||
<field name="product_uom_id" groups="product.group_uom"/>
|
||||
</div>
|
||||
</group>
|
||||
<group>
|
||||
|
||||
@@ -58,4 +58,10 @@
|
||||
</search>
|
||||
</field>
|
||||
</record>
|
||||
<record id="action_stock_history" model="ir.actions.act_window">
|
||||
<field name="name">Stock History</field>
|
||||
<field name="res_model">stock.history</field>
|
||||
<field name="view_type">form</field>
|
||||
<field name="view_mode">tree,graph,pivot</field>
|
||||
</record>
|
||||
</odoo>
|
||||
|
||||
@@ -23,15 +23,21 @@ class WizardValuationHistory(models.TransientModel):
|
||||
history_date=self.date,
|
||||
search_default_group_by_product=True,
|
||||
search_default_group_by_location=True)
|
||||
return {
|
||||
'domain': "[('date', '<=', '" + self.date + "')]",
|
||||
'name': _('Stock Value At Date'),
|
||||
'view_type': 'form',
|
||||
'view_mode': 'tree',
|
||||
'res_model': 'stock.history',
|
||||
'type': 'ir.actions.act_window',
|
||||
'context': ctx,
|
||||
}
|
||||
|
||||
action = self.env['ir.model.data'].xmlid_to_object('stock_account.action_stock_history')
|
||||
if not action:
|
||||
action = {
|
||||
'view_type': 'form',
|
||||
'view_mode': 'tree,graph,pivot',
|
||||
'res_model': 'stock.history',
|
||||
'type': 'ir.actions.act_window',
|
||||
}
|
||||
else:
|
||||
action = action[0].read()[0]
|
||||
|
||||
action['domain'] = "[('date', '<=', '" + self.date + "')]"
|
||||
action['name'] = _('Stock Value At Date')
|
||||
action['context'] = ctx
|
||||
else:
|
||||
action = self.env.ref('stock.quantsact').read()[0]
|
||||
return action
|
||||
return action
|
||||
|
||||
@@ -1214,7 +1214,7 @@ class Export(http.Controller):
|
||||
info = {}
|
||||
fields = self.fields_get(model)
|
||||
if ".id" in export_fields:
|
||||
fields['.id'] = fields.pop('id', {'string': 'ID'})
|
||||
fields['.id'] = fields.get('id', {'string': 'ID'})
|
||||
|
||||
# To make fields retrieval more efficient, fetch all sub-fields of a
|
||||
# given field at the same time. Because the order in the export list is
|
||||
|
||||
@@ -161,26 +161,30 @@ function parse_value (value, descriptor, value_if_empty) {
|
||||
return parse_value(value, {type: "float"});
|
||||
case 'datetime':
|
||||
var datetime = moment(value, [date_pattern + ' ' + time_pattern, date_pattern_wo_zero + ' ' + time_pattern_wo_zero, moment.ISO_8601], true);
|
||||
if (datetime.isValid())
|
||||
if (datetime.isValid() && datetime.year() >= 1900)
|
||||
return time.datetime_to_str(datetime.toDate());
|
||||
datetime = moment(value, [date_pattern + ' ' + time_pattern, date_pattern_wo_zero + ' ' + time_pattern_wo_zero, moment.ISO_8601]);
|
||||
if (datetime.isValid()) {
|
||||
if (datetime.year() === 0) {
|
||||
datetime.year(moment.utc().year());
|
||||
}
|
||||
return time.datetime_to_str(datetime.toDate());
|
||||
if (datetime.year() >= 1900) {
|
||||
return time.datetime_to_str(datetime.toDate());
|
||||
}
|
||||
}
|
||||
throw new Error(_.str.sprintf(_t("'%s' is not a correct datetime"), value));
|
||||
case 'date':
|
||||
var date = moment(value, [date_pattern, date_pattern_wo_zero, moment.ISO_8601], true);
|
||||
if (date.isValid())
|
||||
if (date.isValid() && date.year() >= 1900)
|
||||
return time.date_to_str(date.toDate());
|
||||
date = moment(value, [date_pattern, date_pattern_wo_zero, moment.ISO_8601]);
|
||||
if (date.isValid()) {
|
||||
if (date.year() === 0) {
|
||||
date.year(moment.utc().year());
|
||||
}
|
||||
return time.date_to_str(date.toDate());
|
||||
if (date.year() >= 1900) {
|
||||
return time.date_to_str(date.toDate());
|
||||
}
|
||||
}
|
||||
throw new Error(_.str.sprintf(_t("'%s' is not a correct date"), value));
|
||||
case 'time':
|
||||
|
||||
@@ -1062,8 +1062,10 @@ var One2ManyListView = X2ManyListView.extend({
|
||||
this._dataset_changed = false;
|
||||
});
|
||||
|
||||
this.on('warning', this, function(e) { // In case of a one2many, we do not want any warning which comes from the editor
|
||||
e.stop_propagation();
|
||||
this.on('warning', this, function(e) { // In case of editable list view, we do not want any warning which comes from the editor
|
||||
if (this.editable()) {
|
||||
e.stop_propagation();
|
||||
}
|
||||
});
|
||||
},
|
||||
do_add_record: function () {
|
||||
|
||||
@@ -1502,20 +1502,16 @@ ListView.Groups = Class.extend({
|
||||
});
|
||||
},
|
||||
setup_resequence_rows: function (list, dataset) {
|
||||
var sequence_field = _(this.columns).findWhere({'widget': 'handle'});
|
||||
var seqname = sequence_field ? sequence_field.name : 'sequence';
|
||||
|
||||
// drag and drop enabled if list is not sorted (unless it is sorted by
|
||||
// sequence (ASC)), and there is a visible column with @widget=handle
|
||||
// or "sequence" column in the view.
|
||||
if ((dataset.sort && dataset.sort() && dataset.sort() !== 'sequence'
|
||||
&& dataset.sort() !== 'sequence ASC')
|
||||
|| !_(this.columns).any(function (column) {
|
||||
return column.widget === 'handle'
|
||||
|| column.name === 'sequence'; })) {
|
||||
// its sequence field (ASC)), and there is a visible column with
|
||||
// @widget=handle or "sequence" column in the view.
|
||||
if ((dataset.sort && [seqname, seqname + 'ASC', ''].indexOf(dataset.sort()) === -1)
|
||||
|| !_(this.columns).findWhere({'name': seqname})) {
|
||||
return;
|
||||
}
|
||||
var sequence_field = _(this.columns).find(function (c) {
|
||||
return c.widget === 'handle';
|
||||
});
|
||||
var seqname = sequence_field ? sequence_field.name : 'sequence';
|
||||
|
||||
// ondrop, move relevant record & fix sequences
|
||||
list.$current.sortable({
|
||||
|
||||
@@ -408,6 +408,7 @@ var DataExport = Dialog.extend({
|
||||
},
|
||||
add_field: function(field_id, string) {
|
||||
var $field_list = this.$('.o_fields_list');
|
||||
field_id = this.records[field_id].value || field_id;
|
||||
if($field_list.find("option[value='" + field_id + "']").length === 0) {
|
||||
$field_list.append(new Option(string, field_id));
|
||||
}
|
||||
|
||||
@@ -269,7 +269,7 @@
|
||||
|
||||
<div class="form-group field-password">
|
||||
<label for="password" class="control-label">Password</label>
|
||||
<input type="password" name="password" id="password" class="form-control" required="required" t-att-autofocus="'autofocus' if login else None"/>
|
||||
<input type="password" name="password" id="password" class="form-control" required="required" t-att-autofocus="'autofocus' if login else None" maxlength="4096"/>
|
||||
</div>
|
||||
|
||||
<p class="alert alert-danger" t-if="error">
|
||||
|
||||
@@ -134,7 +134,7 @@ dom.hasProgrammaticStyle = function (node) {
|
||||
};
|
||||
dom.mergeFilter = function (prev, cur, parent) {
|
||||
// merge text nodes
|
||||
if (prev && (dom.isText(prev) || ("H1 H2 H3 H4 H5 H6 LI P".indexOf(prev.tagName) !== -1 && prev !== cur.parentNode)) && dom.isText(cur)) {
|
||||
if (prev && (dom.isText(prev) || (['H1', 'H2', 'H3', 'H4', 'H5', 'H6', 'LI', 'P'].indexOf(prev.tagName) !== -1 && prev !== cur.parentNode)) && dom.isText(cur)) {
|
||||
return true;
|
||||
}
|
||||
if (prev && prev.tagName === "P" && dom.isText(cur)) {
|
||||
|
||||
@@ -371,7 +371,7 @@ class PageConverter(werkzeug.routing.PathConverter):
|
||||
query = query and query.startswith('website.') and query[8:] or query
|
||||
if query:
|
||||
domain += [('key', 'like', query)]
|
||||
website_id = request.context.get('website_id') or request.registry['website'].search(cr, uid, [], limit=1)[0]
|
||||
website_id = request.context.get('website_id') or request.env['website'].search([], limit=1).id
|
||||
domain += ['|', ('website_id', '=', website_id), ('website_id', '=', False)]
|
||||
|
||||
views = View.search_read(domain, fields=['key', 'priority', 'write_date'], order='name')
|
||||
|
||||
@@ -527,7 +527,7 @@ class Website(models.Model):
|
||||
of the same.
|
||||
:rtype: list({name: str, url: str})
|
||||
"""
|
||||
request.context = dict(request.context, **context)
|
||||
request.context = dict(request.context, **self.env.context)
|
||||
router = request.httprequest.app.get_db_router(request.db)
|
||||
# Force enumeration to be performed as public user
|
||||
url_set = set()
|
||||
|
||||
@@ -112,7 +112,6 @@ table.table_desc tr td {
|
||||
height: 100%;
|
||||
.item {
|
||||
height: 100%;
|
||||
background-size: cover;
|
||||
}
|
||||
.carousel-control {
|
||||
cursor: pointer;
|
||||
|
||||
@@ -37,6 +37,16 @@ footer {
|
||||
display: none!important;
|
||||
}
|
||||
|
||||
.o_text_overflow {
|
||||
.text-overflow()
|
||||
}
|
||||
|
||||
.o_div_text_overflow * > div {
|
||||
// .text-overflow() we don't want wordwrap
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
// Odoo options classes
|
||||
|
||||
.jumbotron {
|
||||
|
||||
@@ -77,7 +77,7 @@
|
||||
<t t-call="web.layout">
|
||||
<t t-set="html_data" t-value="{
|
||||
'lang': lang and lang.replace('_', '-'),
|
||||
'data-website-id': website.id if (editable or translatable) and website else None,
|
||||
'data-website-id': website.id if website else None,
|
||||
'data-editable': '1' if editable else None,
|
||||
'data-translatable': '1' if translatable else None,
|
||||
'data-edit_translations': '1' if edit_translations else None,
|
||||
|
||||
@@ -42,7 +42,7 @@ class WebsiteCustomer(http.Controller):
|
||||
|
||||
if tag_id:
|
||||
tag_id = unslug(tag_id)[1] or 0
|
||||
domain += [('tag_ids', 'in', tag_id)]
|
||||
domain += [('website_tag_ids', 'in', tag_id)]
|
||||
|
||||
# group by country, based on customers found with the search(domain)
|
||||
countries = Partner.sudo().read_group(domain, ["id", "country_id"], groupby="country_id", orderby="country_id")
|
||||
|
||||
@@ -51,7 +51,11 @@
|
||||
<span t-field="ticket.price_reduce" t-options='{
|
||||
"widget": "monetary",
|
||||
"display_currency": website.pricelist_id.currency_id
|
||||
}'/>
|
||||
}' groups="sale.group_show_price_subtotal"/>
|
||||
<span t-field="ticket.price_reduce_taxinc" t-options='{
|
||||
"widget": "monetary",
|
||||
"display_currency": website.pricelist_id.currency_id
|
||||
}' groups="sale.group_show_price_total"/>
|
||||
<span itemprop="price" style="display:none;" t-esc="ticket.price"/>
|
||||
<span itemprop="priceCurrency" style="display:none;" t-esc="website.pricelist_id.currency_id.name"/>
|
||||
</t>
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
<field name="arch" type="xml">
|
||||
<button name="toggle_active" position="before">
|
||||
<button class="oe_stat_button" name="website_publish_button"
|
||||
type="object" icon="fa-globe" groups="base.group_hr_user">
|
||||
type="object" icon="fa-globe" groups="hr.group_hr_user">
|
||||
<field name="website_published" widget="website_button"/>
|
||||
</button>
|
||||
</button>
|
||||
|
||||
@@ -93,10 +93,9 @@
|
||||
<span style="font-style:italic;">You’re not alone</span>
|
||||
</p>
|
||||
<p dir="ltr" style="font-size:13px;margin-top:0pt;margin-bottom:8pt;">
|
||||
<span>We would be delighted to assist you along the way. Contact us at
|
||||
<a href="mailto:help@odoo.com"><span style="text-decoration:underline;">
|
||||
help@odoo.com</span></a> if you have any question. You can also discover
|
||||
how to get the best out of Odoo with our </span>
|
||||
<span>We would be delighted to assist you along the way. Contact us through our
|
||||
<a href="https://www.odoo.com/help">support form</a> if you have any question.
|
||||
You can also discover how to get the best out of Odoo with our </span>
|
||||
<a href="https://www.odoo.com/documentation/user">
|
||||
<span style="text-decoration:underline;">User Documentation</span></a>
|
||||
</span><span> or with our </span>
|
||||
|
||||
@@ -133,7 +133,7 @@
|
||||
</template>
|
||||
|
||||
<template id="contact" name="Contact">
|
||||
<h3 class="page-header">Contact</h3>
|
||||
<h3 class="page-header">Saleman</h3>
|
||||
<h5 t-esc="sales_rep.name"/>
|
||||
<p><a t-att-href="'mailto:'+sales_rep.email" t-esc="sales_rep.email"/></p>
|
||||
<p t-esc="sales_rep.phone"/>
|
||||
|
||||
@@ -16,8 +16,13 @@ class PaymentTransaction(models.Model):
|
||||
def _generate_and_pay_invoice(self, tx, acquirer_name):
|
||||
tx.sale_order_id._force_lines_to_invoice_policy_order()
|
||||
|
||||
created_invoice = tx.sale_order_id.action_invoice_create()
|
||||
created_invoice = self.env['account.invoice'].browse(created_invoice)
|
||||
# force company to ensure journals/accounts etc. are correct
|
||||
# company_id needed for default_get on account.journal
|
||||
# force_company needed for company_dependent fields
|
||||
ctx_company = {'company_id': tx.sale_order_id.company_id.id,
|
||||
'force_company': tx.sale_order_id.company_id.id}
|
||||
created_invoice = tx.sale_order_id.with_context(**ctx_company).action_invoice_create()
|
||||
created_invoice = self.env['account.invoice'].browse(created_invoice).with_context(**ctx_company)
|
||||
|
||||
if created_invoice:
|
||||
_logger.info('<%s> transaction completed, auto-generated invoice %s (ID %s) for %s (ID %s)',
|
||||
|
||||
@@ -12,6 +12,8 @@ Odoo E-Commerce
|
||||
""",
|
||||
'depends': ['website', 'sale', 'payment', 'website_payment', 'website_portal_sale', 'website_mail', 'website_form', 'rating'],
|
||||
'data': [
|
||||
'security/ir.model.access.csv',
|
||||
'security/website_sale.xml',
|
||||
'data/data.xml',
|
||||
'data/web_planner_data.xml',
|
||||
'views/views.xml',
|
||||
@@ -24,8 +26,6 @@ Odoo E-Commerce
|
||||
'views/snippets.xml',
|
||||
'views/report_shop_saleorder.xml',
|
||||
'views/res_config_view.xml',
|
||||
'security/ir.model.access.csv',
|
||||
'security/website_sale.xml',
|
||||
],
|
||||
'demo': [
|
||||
'data/demo.xml',
|
||||
|
||||
@@ -302,6 +302,27 @@ class Website(models.Model):
|
||||
DEFAULT_PAYMENT_TERM = 'account.account_payment_term_immediate'
|
||||
return self.env.ref(DEFAULT_PAYMENT_TERM, False).id or partner.property_payment_term_id.id
|
||||
|
||||
@api.multi
|
||||
def _prepare_sale_order_values(self, partner, pricelist):
|
||||
self.ensure_one()
|
||||
affiliate_id = request.session.get('affiliate_id')
|
||||
salesperson_id = affiliate_id if self.env['res.users'].sudo().browse(affiliate_id).exists() else request.website.salesperson_id.id
|
||||
addr = partner.address_get(['delivery', 'invoice'])
|
||||
values = {
|
||||
'partner_id': partner.id,
|
||||
'pricelist_id': pricelist.id,
|
||||
'payment_term_id': self.sale_get_payment_term(partner),
|
||||
'team_id': self.salesteam_id.id,
|
||||
'partner_invoice_id': addr['invoice'],
|
||||
'partner_shipping_id': addr['delivery'],
|
||||
'user_id': salesperson_id or self.salesperson_id.id,
|
||||
}
|
||||
company = self.company_id or pricelist.company_id
|
||||
if company:
|
||||
values['company_id'] = company.id
|
||||
|
||||
return values
|
||||
|
||||
@api.multi
|
||||
def sale_get_order(self, force_create=False, code=None, update_pricelist=False, force_pricelist=False):
|
||||
""" Return the current sales order after mofications specified by params.
|
||||
@@ -337,25 +358,8 @@ class Website(models.Model):
|
||||
# create so if needed
|
||||
if not sale_order and (force_create or code):
|
||||
# TODO cache partner_id session
|
||||
affiliate_id = request.session.get('affiliate_id')
|
||||
if self.env['res.users'].sudo().browse(affiliate_id).exists():
|
||||
salesperson_id = affiliate_id
|
||||
else:
|
||||
salesperson_id = request.website.salesperson_id.id
|
||||
addr = partner.address_get(['delivery', 'invoice'])
|
||||
so_data = {
|
||||
'partner_id': partner.id,
|
||||
'pricelist_id': pricelist_id,
|
||||
'payment_term_id': self.sale_get_payment_term(partner),
|
||||
'team_id': self.salesteam_id.id,
|
||||
'partner_invoice_id': addr['invoice'],
|
||||
'partner_shipping_id': addr['delivery'],
|
||||
'user_id': salesperson_id or self.salesperson_id.id,
|
||||
}
|
||||
company = self.company_id or self.env['product.pricelist'].browse(pricelist_id).sudo().company_id
|
||||
if company:
|
||||
so_data['company_id'] = company.id
|
||||
|
||||
pricelist = self.env['product.pricelist'].browse(pricelist_id).sudo()
|
||||
so_data = self._prepare_sale_order_values(partner, pricelist)
|
||||
sale_order = self.env['sale.order'].sudo().create(so_data)
|
||||
|
||||
# set fiscal position
|
||||
|
||||
@@ -233,7 +233,7 @@ odoo.define('website_sale.website_sale', function (require) {
|
||||
var precision = 2;
|
||||
|
||||
if ($(".decimal_precision").length) {
|
||||
precision = parseInt($(".decimal_precision").first().data('precision'));
|
||||
precision = parseInt($(".decimal_precision").last().data('precision'));
|
||||
}
|
||||
var formatted = _.str.sprintf('%.' + precision + 'f', price).split('.');
|
||||
formatted[0] = utils.insert_thousand_seps(formatted[0]);
|
||||
|
||||
@@ -1735,14 +1735,14 @@
|
||||
<div class="panel panel-info break-word">
|
||||
<div class="panel-heading">Bill To:</div>
|
||||
<div class="panel-body">
|
||||
<div t-field="order.partner_invoice_id" t-options="{'widget': 'contact','fields': ['address', 'name', 'phone', 'email']}" />
|
||||
<div class='o_div_text_overflow' t-field="order.partner_invoice_id" t-options="{'widget': 'contact','fields': ['address', 'name', 'phone', 'email']}" />
|
||||
</div>
|
||||
</div>
|
||||
<t t-if="not order.only_services">
|
||||
<div class="panel panel-info break-word">
|
||||
<div class="panel-heading">Ship To:</div>
|
||||
<div class="panel-body">
|
||||
<div t-field="order.partner_shipping_id" t-options="{'widget': 'contact','fields': ['address', 'name', 'phone']}" />
|
||||
<div class='o_div_text_overflow' t-field="order.partner_shipping_id" t-options="{'widget': 'contact','fields': ['address', 'name', 'phone']}" />
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
import models
|
||||
@@ -0,0 +1 @@
|
||||
import website
|
||||
@@ -0,0 +1,15 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from odoo import api, models
|
||||
|
||||
class Website(models.Model):
|
||||
_inherit = 'website'
|
||||
|
||||
@api.multi
|
||||
def _prepare_sale_order_values(self, partner, pricelist):
|
||||
self.ensure_one()
|
||||
values = super(Website, self)._prepare_sale_order_values(partner, pricelist)
|
||||
if values['company_id']:
|
||||
warehouses = self.env['stock.warehouse'].sudo().search([('company_id', '=', values['company_id'])], limit=1)
|
||||
if warehouses:
|
||||
values['warehouse_id'] = warehouses.id
|
||||
return values
|
||||
@@ -476,7 +476,7 @@ class Slide(models.Model):
|
||||
base_url = self.env['ir.config_parameter'].sudo().get_param('web.base.url')
|
||||
for slide in self.filtered(lambda slide: slide.website_published and slide.channel_id.publish_template_id):
|
||||
publish_template = slide.channel_id.publish_template_id
|
||||
html_body = publish_template.with_context({'base_url': base_url}).render_template(publish_template.body_html, 'slide.slide', slide.id)
|
||||
html_body = publish_template.with_context(base_url=base_url).render_template(publish_template.body_html, 'slide.slide', slide.id)
|
||||
subject = publish_template.render_template(publish_template.subject, 'slide.slide', slide.id)
|
||||
slide.channel_id.message_post(
|
||||
subject=subject,
|
||||
@@ -487,7 +487,7 @@ class Slide(models.Model):
|
||||
@api.one
|
||||
def send_share_email(self, email):
|
||||
base_url = self.env['ir.config_parameter'].sudo().get_param('web.base.url')
|
||||
return self.channel_id.share_template_id.with_context({'email': email, 'base_url': base_url}).send_mail(self.id)
|
||||
return self.channel_id.share_template_id.with_context(email=email, base_url=base_url).send_mail(self.id)
|
||||
|
||||
# --------------------------------------------------
|
||||
# Parsing methods
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
Honduras, 2017-01-12
|
||||
|
||||
Business Analytics Consulting Group S.A. de C.V. agrees to the terms of the Odoo Corporate Contributor License
|
||||
Agreement v1.0.
|
||||
|
||||
I declare that I am authorized and able to make this agreement and sign this
|
||||
declaration.
|
||||
|
||||
Signed,
|
||||
|
||||
Leandro Gabriel Di Pasquale dipasquale@bacgroup.net https://github.com/leodipasquale
|
||||
|
||||
List of contributors:
|
||||
|
||||
Allan Daniel Aguilar Archaga aguilar@bacgroup.net https://github.com/allanaguilar
|
||||
Cesar Alejandro Rodriguez Castillo rodriguez@bacgroup.net https://github.com/hondurasopen
|
||||
Dewin Javier Garcia Armijo garcia@bacgroup.net https://github.com/dewingarcia
|
||||
Jose Reynaldo Cabrera Ayala cabrera@bacgroup.net https://github.com/jcabrera1990
|
||||
Leandro Gabriel Di Pasquale dipasquale@bacgroup.net https://github.com/leodipasquale
|
||||
Edwin Omar McClellan Salinas mcclellan@bacgroup.net https://github.com/prometeomcclellan
|
||||
Salvatore Josue Trimarchi Pinto trimarchi@bacgroup.net https://github.com/salvatoretrimarchi
|
||||
@@ -0,0 +1,11 @@
|
||||
canada, 2017/1/3
|
||||
|
||||
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,
|
||||
|
||||
kevin hua boogerlad@gmail.com https://github.com/boogerlad
|
||||
@@ -0,0 +1,9 @@
|
||||
Chile, 2016-05-31
|
||||
|
||||
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,
|
||||
|
||||
Daniel Santibáñez Polanco dansanti@gmail.com https://github.com/dansanti
|
||||
@@ -0,0 +1,11 @@
|
||||
Greece, 2017-01-22
|
||||
|
||||
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,
|
||||
|
||||
George Daramouskas george.daramouskas@gmail.com https://github.com/daramousk
|
||||
@@ -0,0 +1,11 @@
|
||||
The Czech Republic, 18.01.2017
|
||||
|
||||
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,
|
||||
|
||||
Jiri Zatloukal zatloukal@apitea.com https://github.com/jirizatloukal
|
||||
@@ -0,0 +1,11 @@
|
||||
Norway, 08.01.2017
|
||||
|
||||
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,
|
||||
|
||||
Henrik Norlin henrik.norl@gmail.com https://github.com/norlinhenrik
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user