diff options
| author | Christophe Simonis <chs@odoo.com> | 2017-02-10 13:17:02 +0100 |
|---|---|---|
| committer | Christophe Simonis <chs@odoo.com> | 2017-02-10 13:17:02 +0100 |
| commit | cbdd0b20dcfe1f46706c631262b4eab9996f4eb7 (patch) | |
| tree | cfbe4fd881f9b80ed1da3bca84022607095f5f2d | |
| parent | 3fbaa6c409278014797ce55c6841055b1364d625 (diff) | |
| parent | ffd4442b06bc679a79b107c72806f57da3500d3f (diff) | |
[MERGE] forward port branch 10.0 up to ffd4442b06b
49 files changed, 416 insertions, 117 deletions
diff --git a/addons/account/models/account_invoice.py b/addons/account/models/account_invoice.py index 8ac7194e3d0..f114e2e889d 100644 --- a/addons/account/models/account_invoice.py +++ b/addons/account/models/account_invoice.py @@ -486,7 +486,6 @@ class AccountInvoice(models.Model): addr = self.partner_id.address_get(['delivery']) fiscal_position = self.env['account.fiscal.position'].get_fiscal_position(self.partner_id.id, delivery_id=addr['delivery']) - bank_id = p.bank_ids and p.bank_ids.ids[0] or False # If partner has no warning, check its company if p.invoice_warn == 'no-message' and p.parent_id: @@ -507,8 +506,12 @@ class AccountInvoice(models.Model): self.payment_term_id = payment_term_id self.fiscal_position_id = fiscal_position - if type in ('in_invoice', 'in_refund'): + if type in ('in_invoice', 'out_refund'): + bank_ids = p.commercial_partner_id.bank_ids + bank_id = bank_ids[0].id if bank_ids else False self.partner_bank_id = bank_id + return {'domain': {'partner_bank_id': [('id', 'in', bank_ids.ids)]}} + return {} @api.onchange('journal_id') diff --git a/addons/account/models/account_move.py b/addons/account/models/account_move.py index 0d2569593f1..1252aa647bf 100644 --- a/addons/account/models/account_move.py +++ b/addons/account/models/account_move.py @@ -709,22 +709,37 @@ class AccountMoveLine(models.Model): line_currency = (line.currency_id and line.amount_currency) and line.currency_id or company_currency amount_currency_str = "" total_amount_currency_str = "" - if line_currency != company_currency and target_currency != company_currency: + if line_currency != company_currency and target_currency == line_currency: + # The payment currency is the invoice currency, but they are different than the company currency + # We use the `amount_currency` computed during the invoice validation, at the invoice date + # to avoid exchange gain/loss + # e.g. an invoice of 100€ must be paid with 100€, whatever the company currency and the exchange rates total_amount = line.amount_currency actual_debit = debit > 0 and amount_currency or 0.0 actual_credit = credit > 0 and -amount_currency or 0.0 + currency = line_currency else: + # Either: + # - the invoice, payment, company currencies are all the same, + # - the payment currency is the company currency, but the invoice currency is different, + # - the invoice currency is the company currency, but the payment currency is different, + # - the invoice, payment and company currencies are all different. + # For the two first cases, we can simply use the debit/credit of the invoice move line, which are always in the company currency, + # and this is what the target need. + # For the two last cases, we can use the debit/credit which are in the company currency, and then change them to the target currency total_amount = abs(debit - credit) actual_debit = debit > 0 and amount or 0.0 actual_credit = credit > 0 and -amount or 0.0 - if line_currency != target_currency and target_currency != company_currency: + currency = company_currency + if line_currency != target_currency: amount_currency_str = formatLang(self.env, abs(actual_debit or actual_credit), currency_obj=line_currency) total_amount_currency_str = formatLang(self.env, total_amount, currency_obj=line_currency) + if currency != target_currency: ctx = context.copy() ctx.update({'date': target_date or line.date}) - total_amount = line_currency.with_context(ctx).compute(total_amount, target_currency) - actual_debit = line_currency.with_context(ctx).compute(actual_debit, target_currency) - actual_credit = line_currency.with_context(ctx).compute(actual_credit, target_currency) + total_amount = currency.with_context(ctx).compute(total_amount, target_currency) + actual_debit = currency.with_context(ctx).compute(actual_debit, target_currency) + actual_credit = currency.with_context(ctx).compute(actual_credit, target_currency) amount_str = formatLang(self.env, abs(actual_debit or actual_credit), currency_obj=target_currency) total_amount_str = formatLang(self.env, total_amount, currency_obj=target_currency) diff --git a/addons/account/models/account_payment.py b/addons/account/models/account_payment.py index 34c7e28cac1..75c09b3ac7e 100644 --- a/addons/account/models/account_payment.py +++ b/addons/account/models/account_payment.py @@ -21,7 +21,7 @@ class account_payment_method(models.Model): _name = "account.payment.method" _description = "Payment Methods" - name = fields.Char(required=True) + name = fields.Char(required=True, translate=True) code = fields.Char(required=True) # For internal identification payment_type = fields.Selection([('inbound', 'Inbound'), ('outbound', 'Outbound')], required=True) @@ -400,7 +400,7 @@ 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 = self._compute_total_invoices_amount() + total_residual_company_signed = sum(invoice.residual_company_signed for invoice in self.invoice_ids) total_payment_company_signed = self.currency_id.with_context(date=self.payment_date).compute(self.amount, self.company_id.currency_id) if self.invoice_ids[0].type in ['in_invoice', 'out_refund']: amount_wo = total_payment_company_signed - total_residual_company_signed diff --git a/addons/account/views/account_view.xml b/addons/account/views/account_view.xml index 5f83012ad07..532777a5066 100644 --- a/addons/account/views/account_view.xml +++ b/addons/account/views/account_view.xml @@ -1106,7 +1106,6 @@ <field name="move_id" required="False"/> <newline/> <field name="date_maturity"/> - <field name="date"/> <field name="blocked"/> <newline/> <field name="tax_ids" readonly="1"/> @@ -1195,7 +1194,7 @@ <filter string="Unposted" domain="[('move_id.state','=','draft')]" help="Unposted Journal Items"/> <filter name="posted" string="Posted" domain="[('move_id.state','=','posted')]" help="Posted Journal Items"/> <separator/> - <filter string="Unreconciled" domain="[('full_reconcile_id','=',False), ('account_id.reconcile','=',True)]" help="Journal items where matching number isn't set" name="unreconciled"/> + <filter string="Unreconciled" domain="[('full_reconcile_id', '=', False), ('reconciled','=', False), ('account_id.reconcile','=',True)]" help="Journal items where matching number isn't set" name="unreconciled"/> <separator/> <filter string="Payable" domain="[('account_id.internal_type', '=', 'payable')]" help="From Payable accounts" name="payable"/> <filter string="Receivable" domain="[('account_id.internal_type', '=', 'receivable')]" help="From Receivable accounts" name="receivable"/> diff --git a/addons/crm/data/sales_config_settings_data.xml b/addons/crm/data/sales_config_settings_data.xml index 9aef98293b6..be269cafec9 100644 --- a/addons/crm/data/sales_config_settings_data.xml +++ b/addons/crm/data/sales_config_settings_data.xml @@ -1,8 +1,8 @@ <?xml version="1.0" encoding="utf-8"?> <odoo> - + <data noupdate='1'> <function id="default_generate_sales_team_alias" model="ir.values" name="set_default" eval="('sale.config.settings', 'generate_sales_team_alias', True)"/> - + </data> </odoo> diff --git a/addons/crm/views/crm_lead_views.xml b/addons/crm/views/crm_lead_views.xml index d90664ca1c5..944212cde3e 100644 --- a/addons/crm/views/crm_lead_views.xml +++ b/addons/crm/views/crm_lead_views.xml @@ -583,6 +583,9 @@ <filter string="Overdue Activities" name="activities_overdue" domain="[('activity_ids.date_deadline', '<', context_today().strftime('%Y-%m-%d'))]" help="Show all opportunities for which the next action date is before today"/> + <filter string="Overdue Opportunities" name="overdue_opp" + domain="[('date_deadline', '<', context_today().strftime('%Y-%m-%d'))]" + help="Show all opportunities with a deadline for which the next action date is before today"/> <separator/> <filter string="Unread Messages" name="message_needaction" domain="[('message_needaction','=',True)]"/> <filter string="Lost" domain="[('active', '=', False)]"/> diff --git a/addons/im_livechat/static/src/less/im_livechat.less b/addons/im_livechat/static/src/less/im_livechat.less index c57bfad230a..c52fc7d2b9e 100644 --- a/addons/im_livechat/static/src/less/im_livechat.less +++ b/addons/im_livechat/static/src/less/im_livechat.less @@ -16,6 +16,7 @@ border-bottom: 0px; border-top-left-radius: 5px; border-top-right-radius: 5px; + z-index: 5; } .o_chat_window { diff --git a/addons/l10n_ro/data/account_fiscal_position_data.xml b/addons/l10n_ro/data/account_fiscal_position_data.xml index 8649103fa8f..9ff61aa3f0e 100644 --- a/addons/l10n_ro/data/account_fiscal_position_data.xml +++ b/addons/l10n_ro/data/account_fiscal_position_data.xml @@ -88,6 +88,11 @@ <field name="tax_src_id" ref="tvad_09"/> <field name="tax_dest_id" ref="tvatip09"/> </record> + <record id="afptt_inverse_11" model="account.fiscal.position.tax.template"> + <field name="position_id" ref="fiscal_position_template_2"/> + <field name="tax_src_id" ref="tvad_19"/> + <field name="tax_dest_id" ref="tvatip19"/> + </record> <record id="afptt_inverse_9" model="account.fiscal.position.tax.template"> <field name="position_id" ref="fiscal_position_template_2"/> <field name="tax_src_id" ref="tvad_20"/> @@ -147,6 +152,11 @@ <field name="tax_src_id" ref="tvad_09"/> <field name="tax_dest_id" ref="tvati_intrap9"/> </record> + <record id="afptt_intracom_11" model="account.fiscal.position.tax.template"> + <field name="position_id" ref="fiscal_position_template_3"/> + <field name="tax_src_id" ref="tvad_19"/> + <field name="tax_dest_id" ref="tvati_intrap19"/> + </record> <record id="afptt_intracom_13" model="account.fiscal.position.tax.template"> <field name="position_id" ref="fiscal_position_template_3"/> <field name="tax_src_id" ref="tvad_20"/> @@ -207,6 +217,11 @@ <field name="tax_src_id" ref="tvad_09"/> <field name="tax_dest_id" ref="tvati_intrap9"/> </record> + <record id="afptt_intracoms_11" model="account.fiscal.position.tax.template"> + <field name="position_id" ref="fiscal_position_template_4"/> + <field name="tax_src_id" ref="tvad_19"/> + <field name="tax_dest_id" ref="tvati_intrap19"/> + </record> <record id="afptt_intracoms_13" model="account.fiscal.position.tax.template"> <field name="position_id" ref="fiscal_position_template_4"/> <field name="tax_src_id" ref="tvad_20"/> diff --git a/addons/l10n_ro/data/account_tax_data.xml b/addons/l10n_ro/data/account_tax_data.xml index 8bba8727f27..373fea4c16c 100644 --- a/addons/l10n_ro/data/account_tax_data.xml +++ b/addons/l10n_ro/data/account_tax_data.xml @@ -135,6 +135,21 @@ <field name="applicability">taxes</field> </record> + <record id="tax_tag_tva_taxare_inversa_colectat_19" model="account.account.tag"> + <field name="name">TVA Taxare Inversa Colectat 19%</field> + <field name="applicability">taxes</field> + </record> + + <record id="tax_tag_tva_taxare_inversa_deductibil_19" model="account.account.tag"> + <field name="name">TVA Taxare Inversa Deductibil 19%</field> + <field name="applicability">taxes</field> + </record> + + <record id="tax_tag_tva_taxare_inversa_19" model="account.account.tag"> + <field name="name">TVA Taxare Inversa 19%</field> + <field name="applicability">taxes</field> + </record> + <record id="tax_tag_tva_taxare_inversa_colectat_20" model="account.account.tag"> <field name="name">TVA Taxare Inversa Colectat 20%</field> <field name="applicability">taxes</field> @@ -210,6 +225,21 @@ <field name="applicability">taxes</field> </record> + <record id="tax_tag_tva_intracomunitara_achizitii_colectat_19" model="account.account.tag"> + <field name="name">TVA Intracomunitara Achizitii Colectat 19%</field> + <field name="applicability">taxes</field> + </record> + + <record id="tax_tag_tva_intracomunitara_achizitii_deductibil_19" model="account.account.tag"> + <field name="name">TVA Intracomunitara Achizitii Deductibil 19%</field> + <field name="applicability">taxes</field> + </record> + + <record id="tax_tag_tva_intracomunitara_achizitii_19" model="account.account.tag"> + <field name="name">TVA Intracomunitara Achizitii 19%</field> + <field name="applicability">taxes</field> + </record> + <record id="tax_tag_tva_intracomunitara_achizitii_colectat_20" model="account.account.tag"> <field name="name">TVA Intracomunitara Achizitii Colectat 20%</field> <field name="applicability">taxes</field> @@ -281,7 +311,7 @@ <record id="tvac_19" model="account.tax.template"> <field name="chart_template_id" ref="ro_chart_template"/> - <field name="sequence">11</field> + <field name="sequence">9</field> <field name="name">TVA colectat 19%</field> <field name="description">TVA colectat 19%</field> <field name="amount">19</field> @@ -307,7 +337,7 @@ <record id="tvac_24" model="account.tax.template"> <field name="chart_template_id" ref="ro_chart_template"/> - <field name="sequence">9</field> + <field name="sequence">11</field> <field name="name">TVA colectat 24%</field> <field name="description">TVA colectat 24%</field> <field name="amount">24</field> @@ -359,7 +389,7 @@ <record id="tvad_19" model="account.tax.template"> <field name="chart_template_id" ref="ro_chart_template"/> - <field name="sequence">23</field> + <field name="sequence">19</field> <field name="name">TVA deductibil 19%</field> <field name="description">TVA deductibil 19%</field> <field name="amount">19</field> @@ -385,7 +415,7 @@ <record id="tvad_24" model="account.tax.template"> <field name="chart_template_id" ref="ro_chart_template"/> - <field name="sequence">19</field> + <field name="sequence">23</field> <field name="name">TVA deductibil 24%</field> <field name="description">TVA deductibil 24%</field> <field name="amount">24</field> @@ -506,6 +536,38 @@ <field name="tag_ids" eval="[(6,0,[ref('tax_tag_tva_taxare_inversa_9')])]"/> </record> + <record id="tvatip19-c" model="account.tax.template"> + <field name="chart_template_id" ref="ro_chart_template"/> + <field name="name">TVA Taxare Inversa Colectat 19%</field> + <field name="amount">-19</field> + <field name="amount_type">percent</field> + <field name="type_tax_use">none</field> + <field name="refund_account_id" ref="pcg_4427"/> + <field name="account_id" ref="pcg_4427"/> + <field name="tag_ids" eval="[(6,0,[ref('tax_tag_tva_taxare_inversa_colectat_19')])]"/> + </record> + <record id="tvatip19-d" model="account.tax.template"> + <field name="chart_template_id" ref="ro_chart_template"/> + <field name="name">TVA Taxare Inversa Deductibil 19%</field> + <field name="amount">19</field> + <field name="amount_type">percent</field> + <field name="type_tax_use">none</field> + <field name="refund_account_id" ref="pcg_4426"/> + <field name="account_id" ref="pcg_4426"/> + <field name="tag_ids" eval="[(6,0,[ref('tax_tag_tva_taxare_inversa_deductibil_19')])]"/> + </record> + <record id="tvatip19" model="account.tax.template"> + <field name="chart_template_id" ref="ro_chart_template"/> + <field name="sequence">32</field> + <field name="name">TVA Taxare Inversa 19%</field> + <field name="description">TVA Taxare Inversa 19%</field> + <field name="amount">1</field> + <field name="amount_type">group</field> + <field name="type_tax_use">purchase</field> + <field name="children_tax_ids" eval="[(6, 0, [ref('tvatip19-d'), ref('tvatip19-c')])]"/> + <field name="tag_ids" eval="[(6,0,[ref('tax_tag_tva_taxare_inversa_19')])]"/> + </record> + <record id="tvatip20-c" model="account.tax.template"> <field name="chart_template_id" ref="ro_chart_template"/> <field name="name">TVA Taxare Inversa Colectat 20%</field> @@ -730,6 +792,38 @@ <field name="tag_ids" eval="[(6,0,[ref('tax_tag_tva_intracomunitara_achizitii_9')])]"/> </record> + <record id="tvati_intrap19c" model="account.tax.template"> + <field name="chart_template_id" ref="ro_chart_template"/> + <field name="name">TVA Intracomunitara Achizitii Colectat 19%</field> + <field name="amount">-19</field> + <field name="amount_type">percent</field> + <field name="type_tax_use">none</field> + <field name="refund_account_id" ref="pcg_4427"/> + <field name="account_id" ref="pcg_4427"/> + <field name="tag_ids" eval="[(6,0,[ref('tax_tag_tva_intracomunitara_achizitii_colectat_19')])]"/> + </record> + <record id="tvati_intrap19d" model="account.tax.template"> + <field name="chart_template_id" ref="ro_chart_template"/> + <field name="name">TVA Intracomunitara Achizitii Deductibil 19%</field> + <field name="price_include" eval="0"/> + <field name="amount">19</field> + <field name="amount_type">percent</field> + <field name="type_tax_use">none</field> + <field name="refund_account_id" ref="pcg_4426"/> + <field name="account_id" ref="pcg_4426"/> + <field name="tag_ids" eval="[(6,0,[ref('tax_tag_tva_intracomunitara_achizitii_deductibil_19')])]"/> + </record> + <record id="tvati_intrap19" model="account.tax.template"> + <field name="chart_template_id" ref="ro_chart_template"/> + <field name="sequence">62</field> + <field name="name">TVA Intracomunitara Achizitii 19%</field> + <field name="amount">1</field> + <field name="amount_type">group</field> + <field name="type_tax_use">purchase</field> + <field name="children_tax_ids" eval="[(6, 0, [ref('tvati_intrap19d'), ref('tvati_intrap19c')])]"/> + <field name="tag_ids" eval="[(6,0,[ref('tax_tag_tva_intracomunitara_achizitii_19')])]"/> + </record> + <record id="tvati_intrap20c" model="account.tax.template"> <field name="chart_template_id" ref="ro_chart_template"/> <field name="name">TVA Intracomunitara Achizitii Colectat 20%</field> diff --git a/addons/maintenance/__manifest__.py b/addons/maintenance/__manifest__.py index b30f3bd2d73..9bdde95f507 100644 --- a/addons/maintenance/__manifest__.py +++ b/addons/maintenance/__manifest__.py @@ -15,6 +15,7 @@ 'data/maintenance_data.xml', 'views/maintenance_views.xml', 'views/maintenance_templates.xml', + 'data/maintenance_cron.xml', ], 'demo': ['data/maintenance_demo.xml'], 'installable': True, diff --git a/addons/maintenance/models/maintenance.py b/addons/maintenance/models/maintenance.py index 4d2efbdeaa0..87d6f6535f3 100644 --- a/addons/maintenance/models/maintenance.py +++ b/addons/maintenance/models/maintenance.py @@ -144,17 +144,48 @@ class MaintenanceEquipment(models.Model): maintenance_team_id = fields.Many2one('maintenance.team', string='Maintenance Team') maintenance_duration = fields.Float(help="Maintenance Duration in minutes and seconds.") - @api.depends('period', 'maintenance_open_count', 'maintenance_ids.request_date') + @api.depends('period', 'maintenance_ids.request_date', 'maintenance_ids.close_date') def _compute_next_maintenance(self): - for equipment in self: - create_date = equipment.create_date and datetime.strptime(equipment.create_date, DEFAULT_SERVER_DATETIME_FORMAT) - if equipment.period: - next_date = create_date - if equipment.maintenance_ids: - maintenance = equipment.maintenance_ids.sorted(lambda x: x.request_date)[0] - next_date = maintenance.request_date and datetime.strptime(maintenance.request_date, DEFAULT_SERVER_DATE_FORMAT) or create_date - equipment.next_action_date = next_date and (next_date + timedelta(days=equipment.period)).strftime(DEFAULT_SERVER_DATE_FORMAT) + date_now = fields.Date.context_today(self) + for equipment in self.filtered(lambda x: x.period > 0): + next_maintenance_todo = self.env['maintenance.request'].search([ + ('equipment_id', '=', equipment.id), + ('maintenance_type', '=', 'preventive'), + ('stage_id.done', '!=', True), + ('close_date', '=', False)], order="request_date asc", limit=1) + last_maintenance_done = self.env['maintenance.request'].search([ + ('equipment_id', '=', equipment.id), + ('maintenance_type', '=', 'preventive'), + ('stage_id.done', '=', True), + ('close_date', '!=', False)], order="close_date desc", limit=1) + if next_maintenance_todo and last_maintenance_done: + next_date = next_maintenance_todo.request_date + date_gap = fields.Date.from_string(next_maintenance_todo.request_date) - fields.Date.from_string(last_maintenance_done.close_date) + # If the gap between the last_maintenance_done and the next_maintenance_todo one is bigger than 2 times the period and next request is in the future + # We use 2 times the period to avoid creation too closed request from a manually one created + if date_gap > timedelta(0) and date_gap > timedelta(days=equipment.period) * 2 and fields.Date.from_string(next_maintenance_todo.request_date) > fields.Date.from_string(date_now): + # If the new date still in the past, we set it for today + if fields.Date.from_string(last_maintenance_done.close_date) + timedelta(days=equipment.period) < fields.Date.from_string(date_now): + next_date = date_now + else: + next_date = fields.Date.to_string(fields.Date.from_string(last_maintenance_done.close_date) + timedelta(days=equipment.period)) + elif next_maintenance_todo: + next_date = next_maintenance_todo.request_date + date_gap = fields.Date.from_string(next_maintenance_todo.request_date) - fields.Date.from_string(date_now) + # If next maintenance to do is in the future, and in more than 2 times the period, we insert an new request + # We use 2 times the period to avoid creation too closed request from a manually one created + if date_gap > timedelta(0) and date_gap > timedelta(days=equipment.period) * 2: + next_date = fields.Date.to_string(fields.Date.from_string(date_now)+timedelta(days=equipment.period)) + elif last_maintenance_done: + next_date = fields.Date.from_string(last_maintenance_done.close_date)+timedelta(days=equipment.period) + # If when we add the period to the last maintenance done and we still in past, we plan it for today + if next_date < fields.Date.from_string(date_now): + next_date = date_now + else: + next_date = fields.Date.to_string(fields.Date.from_string(date_now) + timedelta(days=equipment.period)) + + equipment.next_action_date = next_date @api.one @api.depends('maintenance_ids.stage_id.done') def _compute_maintenance_count(self): @@ -190,18 +221,31 @@ class MaintenanceEquipment(models.Model): category_ids = categories._search([], order=order, access_rights_uid=SUPERUSER_ID) return categories.browse(category_ids) + def _create_new_request(self, date): + self.ensure_one() + self.env['maintenance.request'].create({ + 'name': _('Preventive Maintenance - %s') % self.name, + 'request_date': date, + 'schedule_date': date, + 'category_id': self.category_id.id, + 'equipment_id': self.id, + 'maintenance_type': 'preventive', + 'owner_user_id': self.owner_user_id.id, + 'technician_user_id': self.technician_user_id.id, + }) + @api.model def _cron_generate_requests(self): - for equipment in self.search([]): - if equipment.period and equipment.next_action_date == date.today().strftime(DEFAULT_SERVER_DATE_FORMAT): - self.env['maintenance.request'].create({ - 'name': _('Preventive Maintenance - %s' % equipment.next_action_date), - 'request_date': equipment.next_action_date, - 'category_id': equipment.category_id.id, - 'equipment_id': equipment.id, - 'maintenance_type': 'preventive', - }) - + """ + Generates maintenance request on the next_action_date or today if none exists + """ + for equipment in self.search([('period', '>', 0)]): + next_requests = self.env['maintenance.request'].search([('stage_id.done', '=', False), + ('equipment_id', '=', equipment.id), + ('maintenance_type', '=', 'preventive'), + ('request_date', '=', equipment.next_action_date)]) + if not next_requests: + equipment._create_new_request(equipment.next_action_date) class MaintenanceRequest(models.Model): _name = 'maintenance.request' @@ -227,8 +271,8 @@ class MaintenanceRequest(models.Model): name = fields.Char('Subjects', required=True) description = fields.Text('Description') - request_date = fields.Date('Request Date', track_visibility='onchange', default=fields.Date.context_today) - + request_date = fields.Date('Request Date', track_visibility='onchange', default=fields.Date.context_today, + help="Date requested for the maintenance to happen") owner_user_id = fields.Many2one('res.users', string='Created by', default=lambda s: s.env.uid) category_id = fields.Many2one('maintenance.equipment.category', related='equipment_id.category_id', string='Category', store=True, readonly=True) equipment_id = fields.Many2one('maintenance.equipment', string='Equipment', index=True) @@ -237,13 +281,13 @@ class MaintenanceRequest(models.Model): group_expand='_read_group_stage_ids', default=_default_stage) priority = fields.Selection([('0', 'Very Low'), ('1', 'Low'), ('2', 'Normal'), ('3', 'High')], string='Priority') color = fields.Integer('Color Index') - close_date = fields.Date('Close Date') + close_date = fields.Date('Close Date', help="Date the maintenance was finished. ") kanban_state = fields.Selection([('normal', 'In Progress'), ('blocked', 'Blocked'), ('done', 'Ready for next stage')], string='Kanban State', required=True, default='normal', track_visibility='onchange') # active = fields.Boolean(default=True, help="Set active to false to hide the maintenance request without deleting it.") archive = fields.Boolean(default=False, help="Set archive to true to hide the maintenance request without deleting it.") maintenance_type = fields.Selection([('corrective', 'Corrective'), ('preventive', 'Preventive')], string='Maintenance Type', default="corrective") - schedule_date = fields.Datetime('Scheduled Date') + schedule_date = fields.Datetime('Scheduled Date', help="Date the maintenance team plans the maintenance. It should not differ much from the Request Date. ") maintenance_team_id = fields.Many2one('maintenance.team', string='Team', required=True, default=_get_default_team_id) duration = fields.Float(help="Duration in minutes and seconds.") diff --git a/addons/maintenance/tests/test_maintenance.py b/addons/maintenance/tests/test_maintenance.py index a9c55d95ffc..d73301d7ec2 100644 --- a/addons/maintenance/tests/test_maintenance.py +++ b/addons/maintenance/tests/test_maintenance.py @@ -4,7 +4,8 @@ import time from odoo.tests.common import TransactionCase - +from dateutil import relativedelta +import datetime class TestEquipment(TransactionCase): """ Test used to check that when doing equipment/maintenance_request/equipment_category creation.""" @@ -36,7 +37,7 @@ class TestEquipment(TransactionCase): groups_id=[(6, 0, [res_manager.id])] )) - def test_equipment_request_category(self): + def test_10_equipment_request_category(self): # Create a new equipment equipment_01 = self.equipment.sudo(self.manager).create({ @@ -75,3 +76,32 @@ class TestEquipment(TransactionCase): # I check that maintenance request is in the "In Progress" stage self.assertEquals(maintenance_request_01.stage_id.id, self.ref('maintenance.stage_1')) + + def test_20_cron(self): + """ Check the cron creates the necessary preventive maintenance requests""" + equipment_cron = self.equipment.create({ + 'name': 'High Maintenance Monitor because of Color Calibration', + 'category_id': self.ref('maintenance.equipment_monitor'), + 'technician_user_id': self.ref('base.user_root'), + 'owner_user_id': self.user.id, + 'assign_date': time.strftime('%Y-%m-%d'), + 'period': 7, + 'color': 3, + }) + + maintenance_request_cron = self.maintenance_request.create({ + 'name': 'Need a special calibration', + 'technician_user_id': self.user.id, + 'request_date': (datetime.datetime.now() + relativedelta.relativedelta(days=7)).strftime('%Y-%m-%d'), + 'maintenance_type': 'preventive', + 'owner_user_id': self.user.id, + 'equipment_id': equipment_cron.id, + 'color': 7, + 'stage_id': self.ref('maintenance.stage_0'), + 'maintenance_team_id': self.ref('maintenance.equipment_team_maintenance') + }) + + self.env['maintenance.equipment']._cron_generate_requests() + # As it is generating the requests for one month in advance, we should have 4 requests in total + tot_requests = self.maintenance_request.search([('equipment_id', '=', equipment_cron.id)]) + self.assertEqual(len(tot_requests), 1, 'The cron should have generated just 1 request for the High Maintenance Monitor.') diff --git a/addons/maintenance/views/maintenance_views.xml b/addons/maintenance/views/maintenance_views.xml index 99858bff69f..6be82359009 100644 --- a/addons/maintenance/views/maintenance_views.xml +++ b/addons/maintenance/views/maintenance_views.xml @@ -78,7 +78,7 @@ <field name="owner_user_id" string="Requested By"/> <field name="equipment_id" context="{'default_category_id':category_id}"/> <field name="category_id" groups="maintenance.group_equipment_manager"/> - <field name="request_date" groups="base.group_no_one"/> + <field name="request_date"/> <field name="close_date" groups="base.group_no_one"/> <field name="archive" invisible="1"/> <field name="maintenance_type" widget="radio"/> @@ -416,7 +416,7 @@ <div class="oe_kanban_footer_left mt4 label label-danger" t-if="record.maintenance_open_count.raw_value" > <t t-raw="record.maintenance_open_count.raw_value"/> Request </div> - <div class="oe_kanban_footer_left mt4 label label-default" t-if="record.maintenance_open_count.raw_value == 0 and record.next_action_date.raw_value" > + <div class="oe_kanban_footer_left mt4 label label-default" t-if="record.next_action_date.raw_value" > <t t-raw="moment(record.next_action_date.raw_value).format('MMMM Do')"/> </div> </div> diff --git a/addons/mrp/models/mrp_production.py b/addons/mrp/models/mrp_production.py index c57c079be7a..1f772e445f4 100644 --- a/addons/mrp/models/mrp_production.py +++ b/addons/mrp/models/mrp_production.py @@ -323,6 +323,7 @@ class MrpProduction(models.Model): data = { 'name': self.name, 'date': self.date_planned_start, + 'date_expected': self.date_planned_start, 'bom_line_id': bom_line.id, 'product_id': bom_line.product_id.id, 'product_uom_qty': quantity, diff --git a/addons/mrp/models/stock_move.py b/addons/mrp/models/stock_move.py index 33ff2293052..42df10850ca 100644 --- a/addons/mrp/models/stock_move.py +++ b/addons/mrp/models/stock_move.py @@ -81,14 +81,14 @@ class StockMove(models.Model): store=True, help='Technical Field to order moves') # TDE: what ? - @api.multi + @api.depends('state', 'product_uom_qty', 'reserved_availability') def _qty_available(self): for move in self: # For consumables, state is available so availability = qty to do if move.state == 'assigned': move.quantity_available = move.product_uom_qty - else: - move.quantity_available = move.reserved_availability + elif move.product_id.uom_id and move.product_uom: + move.quantity_available = move.product_id.uom_id._compute_quantity(move.reserved_availability, move.product_uom) @api.multi @api.depends('move_lot_ids', 'move_lot_ids.quantity_done', 'quantity_done_store') diff --git a/addons/mrp/wizard/mrp_product_produce_views.xml b/addons/mrp/wizard/mrp_product_produce_views.xml index 9a22d279766..6650449dd16 100644 --- a/addons/mrp/wizard/mrp_product_produce_views.xml +++ b/addons/mrp/wizard/mrp_product_produce_views.xml @@ -17,7 +17,7 @@ <field name="product_uom_id" readonly="1"/> </div> <field name="product_tracking" invisible="1"/> - <field name="lot_id" attrs="{'invisible': [('product_tracking', '=', 'none')]}" domain="[('product_id','=',product_id)]" context="{'default_product_id': product_id}"/> + <field name="lot_id" required="1" attrs="{'invisible': [('product_tracking', '=', 'none')]}" domain="[('product_id','=',product_id)]" context="{'default_product_id': product_id}"/> </group> </group> <group> diff --git a/addons/point_of_sale/models/pos_order.py b/addons/point_of_sale/models/pos_order.py index 201ed22e66c..c2f9a0ca076 100644 --- a/addons/point_of_sale/models/pos_order.py +++ b/addons/point_of_sale/models/pos_order.py @@ -198,6 +198,7 @@ class PosOrder(models.Model): grouped_data = {} have_to_group_by = session and session.config_id.group_by or False + rounding_method = session and session.config_id.company_id.tax_calculation_rounding_method for order in self.filtered(lambda o: not o.account_move or order.state == 'paid'): current_company = order.sale_journal.company_id @@ -296,6 +297,14 @@ class PosOrder(models.Model): 'partner_id': partner_id }) + # round tax lines per order + if rounding_method == 'round_globally': + for group_key, group_value in grouped_data.iteritems(): + if group_key[0] == 'tax': + for line in group_value: + line['credit'] = cur.round(line['credit']) + line['debit'] = cur.round(line['debit']) + # counterpart insert_data('counter_part', { 'name': _("Trade Receivables"), # order.name, diff --git a/addons/point_of_sale/static/src/js/chrome.js b/addons/point_of_sale/static/src/js/chrome.js index f7bf1f78010..02d1123bcc3 100644 --- a/addons/point_of_sale/static/src/js/chrome.js +++ b/addons/point_of_sale/static/src/js/chrome.js @@ -278,13 +278,19 @@ var DebugWidget = PosBaseWidget.extend({ }); this.$('.button.export_unpaid_orders').click(function(){ - self.gui.download_file(self.pos.export_unpaid_orders(), - "unpaid_orders_" + (new Date()).toUTCString().replace(/\ /g,'_') + '.json'); + self.gui.prepare_download_link( + self.pos.export_unpaid_orders(), + _t("unpaid orders") + ' ' + moment().format('YYYY-MM-DD-HH-mm-ss') + '.json', + ".export_unpaid_orders", ".download_unpaid_orders" + ); }); this.$('.button.export_paid_orders').click(function() { - self.gui.download_file(self.pos.export_paid_orders(), - "paid_orders_" + (new Date()).toUTCString().replace(/\ /g,'_') + '.json'); + self.gui.prepare_download_link( + self.pos.export_paid_orders(), + _t("paid orders") + ' ' + moment().format('YYYY-MM-DD-HH-mm-ss') + '.json', + ".export_paid_orders", ".download_paid_orders" + ); }); this.$('.button.import_orders input').on('change', function(event) { diff --git a/addons/point_of_sale/static/src/js/gui.js b/addons/point_of_sale/static/src/js/gui.js index f0755fa7573..87cb30caf78 100644 --- a/addons/point_of_sale/static/src/js/gui.js +++ b/addons/point_of_sale/static/src/js/gui.js @@ -362,7 +362,34 @@ var Gui = core.Class.extend({ // if 'contents' is not a string, it is converted into // a JSON representation of the contents. + + // TODO: remove me in master: deprecated in favor of prepare_download_link + // this method is kept for backward compatibility but is likely not going + // to work as many browsers do to not accept fake click events on links download_file: function(contents, name) { + href_params = this.prepare_file_blob(contents,name); + var evt = document.createEvent("HTMLEvents"); + evt.initEvent("click"); + + $("<a>",href_params).get(0).dispatchEvent(evt); + + }, + + prepare_download_link: function(contents, filename, src, target) { + var href_params = this.prepare_file_blob(contents, filename); + + $(target).parent().attr(href_params); + $(src).addClass('oe_hidden'); + $(target).removeClass('oe_hidden'); + + // hide again after click + $(target).click(function() { + $(src).removeClass('oe_hidden'); + $(this).addClass('oe_hidden'); + }); + }, + + prepare_file_blob: function(contents, name) { var URL = window.URL || window.webkitURL; if (typeof contents !== 'string') { @@ -371,13 +398,8 @@ var Gui = core.Class.extend({ var blob = new Blob([contents]); - var evt = document.createEvent("HTMLEvents"); - evt.initEvent("click"); - - $("<a>",{ - download: name || 'document.txt', - href: URL.createObjectURL(blob), - }).get(0).dispatchEvent(evt); + return {download: name || 'document.txt', + href: URL.createObjectURL(blob),} }, /* ---- Gui: EMAILS ---- */ diff --git a/addons/point_of_sale/static/src/js/popups.js b/addons/point_of_sale/static/src/js/popups.js index f4429840237..eaeae4d6f83 100644 --- a/addons/point_of_sale/static/src/js/popups.js +++ b/addons/point_of_sale/static/src/js/popups.js @@ -105,7 +105,9 @@ var ErrorTracebackPopupWidget = ErrorPopupWidget.extend({ this._super(opts); this.$('.download').off('click').click(function(){ - self.gui.download_file(self.options.body,'traceback.txt'); + self.gui.prepare_download_link(self.options.body, + _t('error') + ' ' + moment().format('YYYY-MM-DD-HH-mm-ss') + '.txt', + '.download', '.download_error_file'); }); this.$('.email').off('click').click(function(){ diff --git a/addons/point_of_sale/static/src/xml/pos.xml b/addons/point_of_sale/static/src/xml/pos.xml index 3245c9a409b..3821fd212dc 100644 --- a/addons/point_of_sale/static/src/xml/pos.xml +++ b/addons/point_of_sale/static/src/xml/pos.xml @@ -336,7 +336,7 @@ </div> <div class='client-detail'> <span class='label'>Country</span> - <select class='detail client-address-country' name='country_id'> + <select class='detail client-address-country needsclick' name='country_id'> <option value=''>None</option> <t t-foreach='widget.pos.countries' t-as='country'> <option t-att-value='country.id' t-att-selected="partner.country_id ? ((country.id === partner.country_id[0]) ? true : undefined) : undefined"> @@ -975,6 +975,9 @@ <div class="button cancel"> Ok </div> + <a><div class="button icon download_error_file oe_hidden"> + <i class="fa fa-arrow-down"></i> + </div></a> <div class="button icon download"> <i class="fa fa-download"></i> </div> @@ -1308,7 +1311,9 @@ <li class="button delete_orders">Delete Paid Orders</li> <li class="button delete_unpaid_orders">Delete Unpaid Orders</li> <li class="button export_paid_orders">Export Paid Orders</li> + <a><li class="button download_paid_orders oe_hidden">Download Paid Orders</li></a> <li class="button export_unpaid_orders">Export Unpaid Orders</li> + <a><li class="button download_unpaid_orders oe_hidden">Download Unpaid Orders</li></a> <li class="button import_orders" style="position:relative"> Import Orders <input type="file" style="opacity:0;position:absolute;top:0;left:0;right:0;bottom:0;margin:0;cursor:pointer"/> diff --git a/addons/point_of_sale/views/pos_order_report_view.xml b/addons/point_of_sale/views/pos_order_report_view.xml index 9559833a92b..43700f170c2 100644 --- a/addons/point_of_sale/views/pos_order_report_view.xml +++ b/addons/point_of_sale/views/pos_order_report_view.xml @@ -4,7 +4,7 @@ <field name="name">report.pos.order.pivot</field> <field name="model">report.pos.order</field> <field name="arch" type="xml"> - <pivot string="Point of Sale Analysis"> + <pivot string="Point of Sale Analysis" disable_linking="True"> <field name="product_categ_id" type="row"/> <field name="date" interval="month" type="col"/> <field name="product_qty" type="measure"/> diff --git a/addons/product/views/product_views.xml b/addons/product/views/product_views.xml index 3bbc078c6cf..7c245ff55dd 100644 --- a/addons/product/views/product_views.xml +++ b/addons/product/views/product_views.xml @@ -44,6 +44,7 @@ <group> <group name="group_general"> <field name="type"/> + <field name="categ_id" string="Internal Category"/> </group> <group name="group_standard_price"> <field name="list_price" widget='monetary' options="{'currency_field': 'currency_id'}"/> @@ -310,6 +311,7 @@ <field name="item_ids" position="replace"> <field name="pricelist_item_ids" widget="many2many"> <tree string="Pricelist Items"> + <field name="pricelist_id"/> <field name="name" string="Applicable On"/> <field name="min_quantity"/> <field name="date_start"/> diff --git a/addons/purchase/models/purchase.py b/addons/purchase/models/purchase.py index c46557ee51f..36e428a7730 100644 --- a/addons/purchase/models/purchase.py +++ b/addons/purchase/models/purchase.py @@ -448,6 +448,7 @@ class PurchaseOrder(models.Model): result = action.read()[0] #override the context to get rid of the default filtering on picking type + result.pop('id', None) result['context'] = {} pick_ids = sum([order.picking_ids.ids for order in self], []) #choose the view_mode accordingly diff --git a/addons/report/models/ir_actions_report_xml.py b/addons/report/models/ir_actions_report_xml.py index 667a2b43349..8461815e389 100644 --- a/addons/report/models/ir_actions_report_xml.py +++ b/addons/report/models/ir_actions_report_xml.py @@ -17,7 +17,7 @@ class ir_actions_report(models.Model): """ self.ensure_one() action_ref = self.env.ref('base.action_ui_view') - if not action_ref: + if not action_ref or len(self.report_name.split('.')) < 2: return False action_data = action_ref.read()[0] action_data['domain'] = [('name', 'ilike', self.report_name.split('.')[1]), ('type', '=', 'qweb')] diff --git a/addons/report_intrastat/views/report_intrastat_views.xml b/addons/report_intrastat/views/report_intrastat_views.xml index ca1c5c67c10..f91c5db3745 100644 --- a/addons/report_intrastat/views/report_intrastat_views.xml +++ b/addons/report_intrastat/views/report_intrastat_views.xml @@ -29,9 +29,11 @@ <field name="model">product.template</field> <field name="inherit_id" ref="stock.view_template_property_form"/> <field name="arch" type="xml"> - <field name="categ_id" position="after"> - <field name="intrastat_id"/> - </field> + <group name="group_lots_and_weight" position="after"> + <group> + <field name="intrastat_id"/> + </group> + </group> </field> </record> diff --git a/addons/sale/models/sale.py b/addons/sale/models/sale.py index 13d1e7a740a..a9d5c3dac20 100644 --- a/addons/sale/models/sale.py +++ b/addons/sale/models/sale.py @@ -506,10 +506,12 @@ class SaleOrder(models.Model): for tax in line.tax_id: group = tax.tax_group_id res.setdefault(group, 0.0) - amount = tax.compute_all(line.price_reduce + base_tax, quantity=line.product_uom_qty)['taxes'][0]['amount'] + amount = tax.compute_all(line.price_reduce + base_tax, quantity=line.product_uom_qty, + product=line.product_id, partner=line.order_partner_id)['taxes'][0]['amount'] res[group] += amount if tax.include_base_amount: - base_tax += tax.compute_all(line.price_reduce + base_tax, quantity=1)['taxes'][0]['amount'] + base_tax += tax.compute_all(line.price_reduce + base_tax, quantity=1, product=line.product_id, + partner=line.order_partner_id)['taxes'][0]['amount'] res = sorted(res.items(), key=lambda l: l[0].sequence) res = map(lambda l: (l[0].name, l[1]), res) return res diff --git a/addons/sales_team/static/src/xml/sales_team_dashboard.xml b/addons/sales_team/static/src/xml/sales_team_dashboard.xml index 93f2f3ca9a9..b1bb4a6ca65 100644 --- a/addons/sales_team/static/src/xml/sales_team_dashboard.xml +++ b/addons/sales_team/static/src/xml/sales_team_dashboard.xml @@ -84,7 +84,7 @@ </a> </td> <td t-if="show_demo or (values and values['closing']['overdue'] != 0)" class="o_warning" title="To Opportunities"> - <a href="#" class="o_dashboard_action" name="crm.action_your_pipeline" data-context="{'search_default_activities_overdue':'1'}"> + <a href="#" class="o_dashboard_action" name="crm.action_your_pipeline" data-context="{'search_default_overdue_opp':'1'}"> <t t-if="!show_demo"> <t t-esc="values['closing']['overdue']"/> exp. closing </t> diff --git a/addons/stock/models/stock_pack_operation.py b/addons/stock/models/stock_pack_operation.py index b6c46310b94..6e51a573546 100644 --- a/addons/stock/models/stock_pack_operation.py +++ b/addons/stock/models/stock_pack_operation.py @@ -34,8 +34,7 @@ class PackOperation(models.Model): product_qty = fields.Float('To Do', default=0.0, digits=dp.get_precision('Product Unit of Measure'), required=True) ordered_qty = fields.Float('Ordered Quantity', digits=dp.get_precision('Product Unit of Measure')) qty_done = fields.Float('Done', default=0.0, digits=dp.get_precision('Product Unit of Measure')) - # TDE FIXME: what what what what ?? - is_done = fields.Boolean(compute='_compute_is_done', inverse='_set_is_done', string='Done', oldname='processed_boolean') + is_done = fields.Boolean(compute='_compute_is_done', string='Done', readonly=False, oldname='processed_boolean') package_id = fields.Many2one('stock.quant.package', 'Source Package') pack_lot_ids = fields.One2many('stock.pack.operation.lot', 'operation_id', 'Lots/Serial Numbers Used') result_package_id = fields.Many2one( @@ -73,9 +72,8 @@ class PackOperation(models.Model): def _compute_is_done(self): self.is_done = self.qty_done > 0.0 - @api.one - def _set_is_done(self): - # TDE FIXME: whuuuut ??? + @api.onchange('is_done') + def on_change_is_done(self): if not self.product_id: if self.is_done and self.qty_done == 0: self.qty_done = 1.0 diff --git a/addons/stock/tests/test_stock_flow.py b/addons/stock/tests/test_stock_flow.py index e0ddb97a84e..3ee20467a88 100644 --- a/addons/stock/tests/test_stock_flow.py +++ b/addons/stock/tests/test_stock_flow.py @@ -1366,7 +1366,8 @@ class TestStockFlow(TestStockCommon): # set the pack in pack operation to 'done' for pack in picking_pack.pack_operation_pack_ids: - pack.qty_done = 1.0 + pack.is_done = True + pack.on_change_is_done() # Put in a pack picking_pack.put_in_pack() @@ -1384,7 +1385,8 @@ class TestStockFlow(TestStockCommon): # set the pack in pack operation to 'done' for pack in picking_out.pack_operation_pack_ids: - pack.qty_done = 1.0 + pack.is_done = True + pack.on_change_is_done() # Validate picking picking_out.do_new_transfer() diff --git a/addons/stock/views/product_views.xml b/addons/stock/views/product_views.xml index f3c05e4dcb1..6fe6c489083 100644 --- a/addons/stock/views/product_views.xml +++ b/addons/stock/views/product_views.xml @@ -140,9 +140,6 @@ <field name="volume" string="Volume"/> </div> </group> - <group> - <field name="categ_id" string="Internal Category"/> - </group> <group name="stock_property" groups="base.group_no_one"> <field name="property_stock_procurement" domain="[('usage','=','procurement')]"/> <field name="property_stock_production" domain="[('usage','=','production')]"/> diff --git a/addons/stock/views/stock_production_lot_views.xml b/addons/stock/views/stock_production_lot_views.xml index 31a2c532a13..cfa5d3929ce 100644 --- a/addons/stock/views/stock_production_lot_views.xml +++ b/addons/stock/views/stock_production_lot_views.xml @@ -15,7 +15,6 @@ <div class="oe_button_box" name="button_box"> <button name="%(lot_open_quants)d" icon="fa-arrows" class="oe_stat_button" string="Locate" type="action" attrs="{'invisible': [('quant_ids','=',[])]}"/> <button name="action_traceability" icon="fa-arrow-up" class="oe_stat_button" string="Traceability" type="object" attrs="{'invisible': [('quant_ids','=',[])]}"/> - <field name="quant_ids" invisible="True"/> </div> <div class="oe_title"> <label for="name" class="oe_edit_only"/> diff --git a/addons/stock_account/report/stock_history.py b/addons/stock_account/report/stock_history.py index 3e67dd1db8d..ccf150954dc 100644 --- a/addons/stock_account/report/stock_history.py +++ b/addons/stock_account/report/stock_history.py @@ -79,7 +79,7 @@ class StockHistory(models.Model): product_template_id, SUM(quantity) as quantity, date, - SUM(price_unit_on_quant * quantity) / SUM(quantity) as price_unit_on_quant, + COALESCE(SUM(price_unit_on_quant * quantity) / NULLIF(SUM(quantity), 0), 0) as price_unit_on_quant, source, string_agg(DISTINCT serial_number, ', ' ORDER BY serial_number) AS serial_number FROM diff --git a/addons/survey/models/survey.py b/addons/survey/models/survey.py index 99ce35e02a3..3d367b0364b 100644 --- a/addons/survey/models/survey.py +++ b/addons/survey/models/survey.py @@ -638,7 +638,7 @@ class SurveyQuestion(models.Model): if self.comments_allowed: comment_answer = answer_candidates.pop(("%s_%s" % (answer_tag, 'comment')), '').strip() # Preventing answers with blank value - if all([True if not answer.strip() else False for answer in answer_candidates.values()]): + if all([True if not answer.strip() else False for answer in answer_candidates.values()]) and answer_candidates: errors.update({answer_tag: self.constr_error_msg}) # There is no answer neither comments (if comments count as answer) if not answer_candidates and self.comment_count_as_answer and (not comment_flag or not comment_answer): diff --git a/addons/web/static/src/js/framework/session.js b/addons/web/static/src/js/framework/session.js index 6af97721926..516436af93c 100644 --- a/addons/web/static/src/js/framework/session.js +++ b/addons/web/static/src/js/framework/session.js @@ -86,8 +86,11 @@ var Session = core.Class.extend(mixins.EventDispatcherMixin, { * Init a session, reloads from cookie, if it exists */ session_init: function () { + var def = this.session_reload(); + if (this.is_frontend) return def; + var self = this; - return this.session_reload().then(function() { + return def.then(function() { var modules = self.module_list.join(','); var deferred = self.load_qweb(modules); if(self.session_is_valid()) { diff --git a/addons/web/static/src/js/views/form_relational_widgets.js b/addons/web/static/src/js/views/form_relational_widgets.js index 46136e21565..06bca6cd382 100644 --- a/addons/web/static/src/js/views/form_relational_widgets.js +++ b/addons/web/static/src/js/views/form_relational_widgets.js @@ -26,7 +26,7 @@ var M2ODialog = Dialog.extend({ title: _.str.sprintf(_t("Create a %s"), parent.string), size: 'medium', buttons: [ - {text: _t('Create'), classes: 'btn-primary', click: function() { + {text: _t('Create'), classes: 'btn-primary', click: function(e) { if (this.$("input").val() !== ''){ this.getParent()._quick_create(this.$("input").val()); this.close(); @@ -997,7 +997,9 @@ var X2ManyListView = ListView.extend({ */ var X2ManyList = ListView.List.extend({ pad_table_to: function (count) { - if (!this.view.is_action_enabled('create') || this.view.x2m.get('effective_readonly')) { + var ftype = this.view.x2m.field.type; + var is_readonly = this.view.x2m.get('effective_readonly'); + if (is_readonly || (ftype === 'one2many' && !this.view.is_action_enabled('create'))) { this._super(count); return; } @@ -1304,7 +1306,7 @@ var Many2ManyListView = X2ManyListView.extend({ context: this.x2m.build_context(), title: _t("Add: ") + this.x2m.string, alternative_form_view: this.x2m.field.views ? this.x2m.field.views.form : undefined, - no_create: this.x2m.options.no_create, + no_create: this.x2m.options.no_create || !this.is_action_enabled('create'), on_selected: function(element_ids) { return self.x2m.data_link_multi(element_ids).then(function() { self.x2m.reload_current_view(); diff --git a/addons/web/static/src/js/views/list_view.js b/addons/web/static/src/js/views/list_view.js index 572fcbff95f..b02c91ed147 100644 --- a/addons/web/static/src/js/views/list_view.js +++ b/addons/web/static/src/js/views/list_view.js @@ -1508,7 +1508,7 @@ ListView.Groups = Class.extend({ // drag and drop enabled if list is not sorted (unless it is sorted by // 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) + if ((dataset.sort && [seqname, seqname + ' ASC', ''].indexOf(dataset.sort()) === -1) || !_(this.columns).findWhere({'name': seqname})) { return; } diff --git a/addons/web/static/src/js/views/pivot_view.js b/addons/web/static/src/js/views/pivot_view.js index 57c26311acc..95e8d13e5ff 100644 --- a/addons/web/static/src/js/views/pivot_view.js +++ b/addons/web/static/src/js/views/pivot_view.js @@ -57,6 +57,7 @@ var PivotView = View.extend({ this.headers = {}; this.cells = {}; this.has_data = false; + this.$buttons = $(); this.last_header_selected = null; this.sorted_column = {}; diff --git a/addons/website_sale_digital/controllers/main.py b/addons/website_sale_digital/controllers/main.py index 312dcca81f2..089f2b97f35 100644 --- a/addons/website_sale_digital/controllers/main.py +++ b/addons/website_sale_digital/controllers/main.py @@ -34,20 +34,16 @@ class WebsiteSaleDigital(website_account): return response order = response.qcontext['order'] invoiced_lines = request.env['account.invoice.line'].sudo().search([('invoice_id', 'in', order.invoice_ids.ids), ('invoice_id.state', '=', 'paid')]) + products = invoiced_lines.mapped('product_id') | order.order_line.filtered(lambda r: not r.price_subtotal).mapped('product_id') purchased_products_attachments = {} - for il in invoiced_lines: - product = il.product_id - # Ignore products that do not have digital content - if not product.product_tmpl_id.type == 'digital': - continue - + for product in products: # Search for product attachments Attachment = request.env['ir.attachment'] product_id = product.id template = product.product_tmpl_id att = Attachment.search_read( - domain=['|', '&', ('res_model', '=', product._name), ('res_id', '=', product_id), '&', ('res_model', '=', template._name), ('res_id', '=', template.id)], + domain=['|', '&', ('res_model', '=', product._name), ('res_id', '=', product_id), '&', ('res_model', '=', template._name), '&', ('res_id', '=', template.id), ('product_downloadable', '=', True)], fields=['name', 'write_date'], order='write_date desc', ) diff --git a/addons/website_sale_digital/models/account_invoice.py b/addons/website_sale_digital/models/account_invoice.py index b6b28026dde..798faca55a0 100644 --- a/addons/website_sale_digital/models/account_invoice.py +++ b/addons/website_sale_digital/models/account_invoice.py @@ -13,7 +13,13 @@ class AccountInvoiceLine(models.Model): # Get paid invoices purchases = self.sudo().search_read( - domain=[('invoice_id.state', '=', 'paid'), ('invoice_id.partner_id', '=', partner.id), ('product_id.product_tmpl_id.type', '=', 'digital')], + domain=[('invoice_id.state', '=', 'paid'), ('invoice_id.partner_id', '=', partner.id)], + fields=['product_id'], + ) + + # Get free products + purchases += self.env['sale.order.line'].sudo().search_read( + domain=[('price_subtotal', '=', 0.0), ('order_id.partner_id', '=', partner.id)], fields=['product_id'], ) diff --git a/addons/website_slides/models/slides.py b/addons/website_slides/models/slides.py index 977a39316b5..91211ea79f4 100644 --- a/addons/website_slides/models/slides.py +++ b/addons/website_slides/models/slides.py @@ -494,11 +494,12 @@ class Slide(models.Model): # -------------------------------------------------- @api.model - def _fetch_data(self, base_url, data, content_type=False): + def _fetch_data(self, base_url, data, content_type=False, extra_params=False): result = {'values': dict()} try: if data: - base_url = base_url + '?%s' % urlencode(data) + sep = '?' if not extra_params else '&' + base_url = base_url + '%s%s' % (sep, urlencode(data)) req = urllib2.Request(base_url) content = urllib2.urlopen(req).read() if content_type == 'json': @@ -566,12 +567,13 @@ class Slide(models.Model): def _parse_google_document(self, document_id, only_preview_fields): def get_slide_type(vals): # TDE FIXME: WTF ?? - image = Image.open(io.BytesIO(vals['image'].decode('base64'))) - width, height = image.size - if height > width: - return 'document' - else: - return 'presentation' + slide_type = 'presentation' + if vals.get('image'): + image = Image.open(io.BytesIO(vals['image'].decode('base64'))) + width, height = image.size + if height > width: + return 'document' + return slide_type # Google drive doesn't use a simple API key to access the data, but requires an access # token. However, this token is generated in module google_drive, which is not in the @@ -609,12 +611,14 @@ class Slide(models.Model): values['datas'] = values['image'] values['slide_type'] = 'infographic' elif google_values['mimeType'].startswith('application/vnd.google-apps'): - values['datas'] = self._fetch_data(google_values['exportLinks']['application/pdf'], {}, 'pdf')['values'] values['slide_type'] = get_slide_type(values) - if google_values['exportLinks'].get('text/plain'): - values['index_content'] = self._fetch_data(google_values['exportLinks']['text/plain'], {})['values'] - if google_values['exportLinks'].get('text/csv'): - values['index_content'] = self._fetch_data(google_values['exportLinks']['text/csv'], {})['values'] + if 'exportLinks' in google_values: + values['datas'] = self._fetch_data(google_values['exportLinks']['application/pdf'], params, 'pdf', extra_params=True)['values'] + # Content indexing + if google_values['exportLinks'].get('text/plain'): + values['index_content'] = self._fetch_data(google_values['exportLinks']['text/plain'], params, extra_params=True)['values'] + elif google_values['exportLinks'].get('text/csv'): + values['index_content'] = self._fetch_data(google_values['exportLinks']['text/csv'], params, extra_params=True)['values'] elif google_values['mimeType'] == 'application/pdf': # TODO: Google Drive PDF document doesn't provide plain text transcript values['datas'] = self._fetch_data(google_values['webContentLink'], {}, 'pdf')['values'] diff --git a/doc/cla/corporate/somko.md b/doc/cla/corporate/somko.md new file mode 100644 index 00000000000..0c864ab8d24 --- /dev/null +++ b/doc/cla/corporate/somko.md @@ -0,0 +1,17 @@ +Belgium, 30/01/2017 + +Somko 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, + +Frederik Santens info@somko.be https://github.com/somko + +List of contributors: + +Frederik Santens info@somko.be https://github.com/somko +Thomas Vanmellaerts thomas.vanmellaerts@somko.be https://github.com/thomassomko +Kasper Declercq kasper.declercq@somko.be https://github.com/kasperdeclercq
\ No newline at end of file diff --git a/doc/cla/individual/yueqin.md b/doc/cla/individual/yueqin.md new file mode 100644 index 00000000000..972acd4fa8f --- /dev/null +++ b/doc/cla/individual/yueqin.md @@ -0,0 +1,11 @@ + + +China, 2017-02-06 + +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, + +Yue Qin 602559817@qq.com https://github.com/yueqin diff --git a/doc/setup/install.rst b/doc/setup/install.rst index 9d90fef01d7..3a8d45afa89 100644 --- a/doc/setup/install.rst +++ b/doc/setup/install.rst @@ -207,9 +207,9 @@ RPM .. warning:: - with RHEL-based distributions (RHEL, CenOS, Scientific Linux), EPEL_ must + with RHEL-based distributions (RHEL, CentOS, Scientific Linux), EPEL_ must be added to the distribution's repositories for all of Odoo's - dependencies to be available. For CenOS: + dependencies to be available. For CentOS: .. code-block:: console diff --git a/odoo/addons/base/ir/ir_actions.py b/odoo/addons/base/ir/ir_actions.py index 44df2bc4770..1b2cbad555d 100644 --- a/odoo/addons/base/ir/ir_actions.py +++ b/odoo/addons/base/ir/ir_actions.py @@ -611,10 +611,11 @@ class IrActionsServer(models.Model): :type action: browse record :returns: dict -- evaluation context given to (safe_)safe_eval """ def log(message, level="info"): - self._cr.execute(""" - INSERT INTO ir_logging(create_date, create_uid, type, dbname, name, level, message, path, line, func) - VALUES (NOW() at time zone 'UTC', %s, %s, %s, %s, %s, %s, %s, %s, %s) - """, (self.env.uid, 'server', self._cr.dbname, __name__, level, message, "action", action.id, action.name)) + with self.pool.cursor() as cr: + cr.execute(""" + INSERT INTO ir_logging(create_date, create_uid, type, dbname, name, level, message, path, line, func) + VALUES (NOW() at time zone 'UTC', %s, %s, %s, %s, %s, %s, %s, %s, %s) + """, (self.env.uid, 'server', self._cr.dbname, __name__, level, message, "action", action.id, action.name)) eval_context = super(IrActionsServer, self)._get_eval_context(action=action) model = self.env[action.model_id.model] diff --git a/odoo/addons/base/ir/ir_model.py b/odoo/addons/base/ir/ir_model.py index 4785359063b..5189d7360f7 100644 --- a/odoo/addons/base/ir/ir_model.py +++ b/odoo/addons/base/ir/ir_model.py @@ -1340,7 +1340,7 @@ class IrModelData(models.Model): class WizardModelMenu(models.TransientModel): _name = 'wizard.ir.model.menu.create' - menu_id = fields.Many2one('ir.ui.menu', string='Parent Menu', required=True) + menu_id = fields.Many2one('ir.ui.menu', string='Parent Menu', required=True, ondelete='cascade') name = fields.Char(string='Menu Name', required=True) @api.multi diff --git a/odoo/addons/base/ir/ir_ui_menu.py b/odoo/addons/base/ir/ir_ui_menu.py index b319f77f909..6a7aaa8a482 100644 --- a/odoo/addons/base/ir/ir_ui_menu.py +++ b/odoo/addons/base/ir/ir_ui_menu.py @@ -55,7 +55,7 @@ class IrUiMenu(models.Model): if level <= 0: return '...' if self.parent_id: - return self.parent_id._get_full_name(level - 1) + MENU_ITEM_SEPARATOR + self.name + return self.parent_id._get_full_name(level - 1) + MENU_ITEM_SEPARATOR + (self.name or "") else: return self.name diff --git a/odoo/addons/base/res/res_partner.py b/odoo/addons/base/res/res_partner.py index c8bcaa042ad..1601819ae94 100644 --- a/odoo/addons/base/res/res_partner.py +++ b/odoo/addons/base/res/res_partner.py @@ -430,6 +430,7 @@ class Partner(models.Model): sync_children = self.child_ids.filtered(lambda c: not c.is_company) for child in sync_children: child._commercial_sync_to_children() + sync_children._compute_commercial_partner() return sync_children.write(sync_vals) @api.multi @@ -453,6 +454,10 @@ class Partner(models.Model): commercial_fields = self._commercial_fields() if any(field in values for field in commercial_fields): self._commercial_sync_to_children() + for child in self.child_ids.filtered(lambda c: not c.is_company): + if child.commercial_partner_id != self.commercial_partner_id : + self._commercial_sync_to_children() + break # 2b. Address fields: sync if address changed address_fields = self._address_fields() if any(field in values for field in address_fields): diff --git a/odoo/models.py b/odoo/models.py index c5a6599f5b5..f511b7cce33 100644 --- a/odoo/models.py +++ b/odoo/models.py @@ -2001,7 +2001,7 @@ class BaseModel(object): prefix_term = lambda prefix, term: ('%s %s' % (prefix, term)) if term else '' query = """ - SELECT min(%(table)s.id) AS id, count(%(table)s.id) AS %(count_field)s %(extra_fields)s + SELECT min("%(table)s".id) AS id, count("%(table)s".id) AS "%(count_field)s" %(extra_fields)s FROM %(from)s %(where)s %(groupby)s |
