diff --git a/addons/account/data/account_data.xml b/addons/account/data/account_data.xml index 84b208c3430..09daa42bbeb 100644 --- a/addons/account/data/account_data.xml +++ b/addons/account/data/account_data.xml @@ -51,6 +51,7 @@ A + @@ -498,7 +498,7 @@ - + (standard)Output Purchase Tax @ 12% @@ -510,7 +510,7 @@ - + (standard)Output Purchase Tax @ 4% diff --git a/addons/l10n_in_hr_payroll/data/hr.salary.rule.csv b/addons/l10n_in_hr_payroll/data/hr.salary.rule.csv index 043ba8f0522..caeb3e9ec60 100644 --- a/addons/l10n_in_hr_payroll/data/hr.salary.rule.csv +++ b/addons/l10n_in_hr_payroll/data/hr.salary.rule.csv @@ -1,5 +1,5 @@ "id","amount_select","condition_range_min","condition_range_max","amount_percentage","amount_fix","name","category_id/id","sequence","code","parent_rule_id/id","condition_select","condition_range","amount_percentage_base" 1,"fix",1,1,,100,"Education Allowance For One Child",hr_payroll.ALW,23,"CHEAONE","hr_payroll_rule_child1","range","employee.children", -2,"fix",2,10,,200,"Education Allowance For Two Child",hr_payroll.ALW,24,"CHEATWO","hr_payroll_rule_child1","range","employee.children", +2,"fix",2,10,,200,"Education Allowance For Two Children",hr_payroll.ALW,24,"CHEATWO","hr_payroll_rule_child1","range","employee.children", 3,"fix",1,1,,300,"Child Hostel Allowance For One Child",hr_payroll.ALW,26,"CHOONE","hr_payroll_rule_child2","range","employee.children", -4,"fix",2,10,,600,"Child Hostel Allowance For Two Child",hr_payroll.ALW,27,"CHOTWO","hr_payroll_rule_child2","range","employee.children", +4,"fix",2,10,,600,"Child Hostel Allowance For Two Children",hr_payroll.ALW,27,"CHOTWO","hr_payroll_rule_child2","range","employee.children", diff --git a/addons/l10n_in_hr_payroll/data/l10n_in_hr_payroll_data.xml b/addons/l10n_in_hr_payroll/data/l10n_in_hr_payroll_data.xml index 6664d080bff..17ec10ad01d 100644 --- a/addons/l10n_in_hr_payroll/data/l10n_in_hr_payroll_data.xml +++ b/addons/l10n_in_hr_payroll/data/l10n_in_hr_payroll_data.xml @@ -70,7 +70,7 @@ HRAMN code result = (payslip.company_id.dearness_allowance and -((contract.wage + DA) * contract.house_rent_allowance_metro_nonmetro / 100) +((contract.wage + categories.DA) * contract.house_rent_allowance_metro_nonmetro / 100) or (contract.wage * contract.house_rent_allowance_metro_nonmetro / 100)) House Rent Allowance @@ -130,7 +130,7 @@ or (contract.wage * contract.house_rent_allowance_metro_nonmetro / 100)) none code - result = (payslip.company_id.dearness_allowance and ((contract.wage + DA) * 0.10) + result = (payslip.company_id.dearness_allowance and ((contract.wage + categories.DA) * 0.10) or (contract.wage * 0.10)) This allowance is paid to Employees who are posted in big cities. The purpose is to compensate the high cost of living in cities like Mumbai, Delhi, etc. However it is Fully Taxable. @@ -365,7 +365,7 @@ else -0) Employee's PF Contribution code - result = (payslip.company_id.dearness_allowance and - ((contract.wage + DA) * 0.12) + result = (payslip.company_id.dearness_allowance and - ((contract.wage + categories.DA) * 0.12) or - (contract.wage * 0.12)) Employer contribution does not become part of employee’s income and hence income tax is not payable on this part. @@ -377,7 +377,7 @@ or - (contract.wage * 0.12)) Employee's NPS Contribution code - result = (payslip.company_id.dearness_allowance and - ((contract.wage + DA) * 0.10) + result = (payslip.company_id.dearness_allowance and - ((contract.wage + categories.DA) * 0.10) or - (contract.wage * 0.10)) @@ -392,7 +392,7 @@ or - (contract.wage * 0.10)) result = bool(contract.voluntary_provident_fund) code result = (payslip.company_id.dearness_allowance and -- ((contract.wage + DA) * contract.voluntary_provident_fund / 100) +- ((contract.wage + categories.DA) * contract.voluntary_provident_fund / 100) or - (contract.wage * contract.voluntary_provident_fund / 100)) @@ -498,7 +498,7 @@ or - (contract.wage * contract.voluntary_provident_fund / 100)) Employer's NPS Contribution code - result = (payslip.company_id.dearness_allowance and - ((contract.wage + DA) * 0.10) + result = (payslip.company_id.dearness_allowance and - ((contract.wage + categories.DA) * 0.10) or - (contract.wage * 0.10)) @@ -511,7 +511,7 @@ or - (contract.wage * 0.10)) Employer's PF Contribution code - result = (payslip.company_id.dearness_allowance and - ((contract.wage + DA) * 0.12) + result = (payslip.company_id.dearness_allowance and - ((contract.wage + categories.DA) * 0.12) or - (contract.wage * 0.12)) diff --git a/addons/l10n_mx/data/account_tax_data.xml b/addons/l10n_mx/data/account_tax_data.xml index c5e300e1275..8bd16cf1dc7 100644 --- a/addons/l10n_mx/data/account_tax_data.xml +++ b/addons/l10n_mx/data/account_tax_data.xml @@ -2,174 +2,138 @@ - - IVA(0%) VENTAS + + IVA taxes - - IVA(16%) VENTAS + + ISR taxes - - RET IVA FLETES 4% - taxes - - - RET IVA ARRENDAMIENTO 10% - taxes - - - RET ISR ARRENDAMIENTO 10% - taxes - - - RET ISR HONORARIOS 10% - taxes - - - RETENCION IVA ARRENDAMIENTO 10.67% - taxes - - - RETENCION IVA HONORARIOS 10.67% - taxes - - - IVA(0%) COMPRAS - taxes - - - IVA(16%) COMPRAS + + IEPS taxes IVA(0%) VENTAS - IVA + IVA(0%) 0 percent sale - + IVA(16%) VENTAS - IVA + IVA(16%) 16 percent sale - + RET IVA FLETES 4% - ITAXR_04-OUT + Retención IVA(-4%) -4 percent purchase - RET IVA ARRENDAMIENTO 10% - ITAXR_10-OUT + Retención IVA(-10%) -10 percent purchase - RET ISR ARRENDAMIENTO 10% - ITAXA_10-OUT + Retención ISR(-10%) -10 percent purchase - RET ISR HONORARIOS 10% - ITAXH_10-OUT + Retención ISR(-10%) -10 percent purchase - RETENCION IVA ARRENDAMIENTO 10.67% - ITAX_1067-OUT + Retención IVA(-10.67%) -10.67 percent purchase - RETENCION IVA HONORARIOS 10.67% - ITAX_167-OUT + Retención IVA(-10.67%) -10.67 percent purchase - IVA(0%) COMPRAS - ITAX_010-OUT + IVA(0%) 0 percent purchase - IVA(16%) COMPRAS - ITAX_160-OUT + IVA(16%) 16 percent purchase - diff --git a/addons/l10n_mx/data/fiscal_position_data.xml b/addons/l10n_mx/data/fiscal_position_data.xml index d2ad94d8613..32589454dad 100644 --- a/addons/l10n_mx/data/fiscal_position_data.xml +++ b/addons/l10n_mx/data/fiscal_position_data.xml @@ -1,39 +1,39 @@ - + Personas morales del régimen general - + Personas morales con fines no lucrativos - + Asociaciones Religiosas - + Personas morales del régimen simplificado - + Persona física con actividad empresarial - + Foreign Customer - - - + + + - - - + + + diff --git a/addons/l10n_nl/__manifest__.py b/addons/l10n_nl/__manifest__.py index cecd5c9ca72..0016847414f 100644 --- a/addons/l10n_nl/__manifest__.py +++ b/addons/l10n_nl/__manifest__.py @@ -6,7 +6,7 @@ { 'name': 'Netherlands - Accounting', 'version': '3.0', - 'category': 'Localization/Account Charts', + 'category': 'Localization', 'author': 'ONESTEiN BV', 'website': 'http://www.onestein.eu', 'depends': [ 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 @@ + + + + + @@ -147,6 +152,11 @@ + + + + + @@ -207,6 +217,11 @@ + + + + + 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 @@ taxes + + TVA Taxare Inversa Colectat 19% + taxes + + + + TVA Taxare Inversa Deductibil 19% + taxes + + + + TVA Taxare Inversa 19% + taxes + + TVA Taxare Inversa Colectat 20% taxes @@ -210,6 +225,21 @@ taxes + + TVA Intracomunitara Achizitii Colectat 19% + taxes + + + + TVA Intracomunitara Achizitii Deductibil 19% + taxes + + + + TVA Intracomunitara Achizitii 19% + taxes + + TVA Intracomunitara Achizitii Colectat 20% taxes @@ -281,7 +311,7 @@ - 11 + 9 TVA colectat 19% TVA colectat 19% 19 @@ -307,7 +337,7 @@ - 9 + 11 TVA colectat 24% TVA colectat 24% 24 @@ -359,7 +389,7 @@ - 23 + 19 TVA deductibil 19% TVA deductibil 19% 19 @@ -385,7 +415,7 @@ - 19 + 23 TVA deductibil 24% TVA deductibil 24% 24 @@ -506,6 +536,38 @@ + + + TVA Taxare Inversa Colectat 19% + -19 + percent + none + + + + + + + TVA Taxare Inversa Deductibil 19% + 19 + percent + none + + + + + + + 32 + TVA Taxare Inversa 19% + TVA Taxare Inversa 19% + 1 + group + purchase + + + + TVA Taxare Inversa Colectat 20% @@ -730,6 +792,38 @@ + + + TVA Intracomunitara Achizitii Colectat 19% + -19 + percent + none + + + + + + + TVA Intracomunitara Achizitii Deductibil 19% + + 19 + percent + none + + + + + + + 62 + TVA Intracomunitara Achizitii 19% + 1 + group + purchase + + + + TVA Intracomunitara Achizitii Colectat 20% diff --git a/addons/mail/models/mail_channel.py b/addons/mail/models/mail_channel.py index e1c7a1b458e..63bd82a5f87 100644 --- a/addons/mail/models/mail_channel.py +++ b/addons/mail/models/mail_channel.py @@ -209,8 +209,9 @@ class Channel(models.Model): @api.multi def message_receive_bounce(self, email, partner, mail_id=None): """ Override bounce management to unsubscribe bouncing addresses """ - if partner.message_bounce >= self.MAX_BOUNCE_LIMIT: - self._action_unfollow(partner) + for p in partner: + if p.message_bounce >= self.MAX_BOUNCE_LIMIT: + self._action_unfollow(p) return super(Channel, self).message_receive_bounce(email, partner, mail_id=mail_id) @api.multi diff --git a/addons/mail/models/mail_thread.py b/addons/mail/models/mail_thread.py index 7bbf35809a4..d08158750dd 100644 --- a/addons/mail/models/mail_thread.py +++ b/addons/mail/models/mail_thread.py @@ -1049,19 +1049,20 @@ class MailThread(models.AbstractModel): bounce_match = bounce_re.search(email_to) if bounce_match: - bounced_mail_id, bounced_model, bounced_thread_id = bounce_match.group(1), bounce_match.group(2), int(bounce_match.group(3)) + bounced_mail_id, bounced_model, bounced_thread_id = bounce_match.group(1), bounce_match.group(2), bounce_match.group(3) email_part = next((part for part in message.walk() if part.get_content_type() == 'message/rfc822'), None) dsn_part = next((part for part in message.walk() if part.get_content_type() == 'message/delivery-status'), None) - partner, partner_address = self.env['res.partner'], False + partners, partner_address = self.env['res.partner'], False if dsn_part and len(dsn_part.get_payload()) > 1: dsn = dsn_part.get_payload()[1] final_recipient_data = tools.decode_message_header(dsn, 'Final-Recipient') partner_address = final_recipient_data.split(';', 1)[1].strip() if partner_address: - partner = partner.sudo().search([('email', 'like', partner_address)]) - partner.message_receive_bounce(partner_address, partner, mail_id=bounced_mail_id) + partners = partners.sudo().search([('email', 'like', partner_address)]) + for partner in partners: + partner.message_receive_bounce(partner_address, partner, mail_id=bounced_mail_id) mail_message = self.env['mail.message'] if email_part: @@ -1069,19 +1070,19 @@ class MailThread(models.AbstractModel): bounced_message_id = tools.mail_header_msgid_re.findall(tools.decode_message_header(email, 'Message-Id')) mail_message = MailMessage.sudo().search([('message_id', 'in', bounced_message_id)]) - if partner and mail_message: + if partners and mail_message: notifications = self.env['mail.notification'].sudo().search([ ('mail_message_id', '=', mail_message.id), - ('res_partner_id', '=', partner.id)]) + ('res_partner_id', 'in', partners.ids)]) notifications.write({ 'email_status': 'bounce' }) if bounced_model in self.env and hasattr(self.env[bounced_model], 'message_receive_bounce') and bounced_thread_id: - self.env[bounced_model].browse(bounced_thread_id).message_receive_bounce(partner_address, partner, mail_id=bounced_mail_id) + self.env[bounced_model].browse(int(bounced_thread_id)).message_receive_bounce(partner_address, partners, mail_id=bounced_mail_id) _logger.info('Routing mail from %s to %s with Message-Id %s: bounced mail from mail %s, model: %s, thread_id: %s: dest %s (partner %s)', - email_from, email_to, message_id, bounced_mail_id, bounced_model, bounced_thread_id, partner_address, partner.id) + email_from, email_to, message_id, bounced_mail_id, bounced_model, bounced_thread_id, partner_address, partners) return [] # 0. First check if this is a bounce message or not. diff --git a/addons/mail/static/src/js/activity.js b/addons/mail/static/src/js/activity.js index aa929445488..dd1b46c8d4f 100644 --- a/addons/mail/static/src/js/activity.js +++ b/addons/mail/static/src/js/activity.js @@ -66,6 +66,7 @@ var Activity = form_common.AbstractField.extend({ this._super.apply(this, arguments); this.context = this.options.context || {}; + this.model = this.view.dataset.model; this.activities = []; this.Activity = new Model('mail.activity'); }, @@ -98,7 +99,7 @@ var Activity = form_common.AbstractField.extend({ fetch_and_render_value: function () { var self = this; - return new Model(this.view.dataset.model) + return new Model(this.model) .call("read", [[this.get_res_id()], ['activity_ids']]) .then(function (results) { self.value = results[0]['activity_ids']; @@ -133,7 +134,7 @@ var Activity = form_common.AbstractField.extend({ target: 'new', context: _.extend({ default_res_id: this.get_res_id(), - default_res_model: this.view.dataset.model, + default_res_model: this.model, }, {'mark_done': true}, this.context), res_id: false, }; @@ -141,7 +142,7 @@ var Activity = form_common.AbstractField.extend({ on_close: function() { self.fetch_and_render_value(); self.chatter.refresh_followers(); - ChatManager.get_messages({model: self.res_model, res_id: self.get_res_id()}); + ChatManager.get_messages({model: self.model, res_id: self.get_res_id()}); }, }); }, @@ -169,7 +170,7 @@ var Activity = form_common.AbstractField.extend({ on_close: function () { self.fetch_and_render_value(); self.chatter.refresh_followers(); - ChatManager.get_messages({model: self.res_model, res_id: self.get_res_id()}); + ChatManager.get_messages({model: self.model, res_id: self.get_res_id()}); }, }); }, diff --git a/addons/mail/static/src/js/chat_manager.js b/addons/mail/static/src/js/chat_manager.js index 146196af6c9..93c1cf07075 100644 --- a/addons/mail/static/src/js/chat_manager.js +++ b/addons/mail/static/src/js/chat_manager.js @@ -1045,7 +1045,9 @@ function init () { bus.on('notification', null, on_notification); - return session.rpc('/mail/client_action').then(function (result) { + return session.is_bound.then(function(){ + return session.rpc('/mail/client_action'); + }).then(function (result) { _.each(result.channel_slots, function (channels) { _.each(channels, add_channel); }); 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 14661e62322..a055b24f82f 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/security/maintenance.xml b/addons/maintenance/security/maintenance.xml index cf564b0c859..a567eb8ddab 100644 --- a/addons/maintenance/security/maintenance.xml +++ b/addons/maintenance/security/maintenance.xml @@ -12,14 +12,14 @@ Users are allowed to access their own maintenance requests - ['|', ('owner_user_id.id', '=', user.id), ('technician_user_id.id', '=', user.id)] + ['|', ('message_partner_ids', 'in', [user.partner_id.id]), ('technician_user_id.id', '=', user.id)] - Users are allowed to access their own equipments + Users are allowed to access equipments they follow - [('owner_user_id.id', '=', user.id)] + [('message_partner_ids', 'in', [user.partner_id.id])] 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 @@ - + @@ -416,7 +416,7 @@ - diff --git a/addons/mass_mailing/wizard/test_mailing.py b/addons/mass_mailing/wizard/test_mailing.py index 17473464126..130827e1fe4 100644 --- a/addons/mass_mailing/wizard/test_mailing.py +++ b/addons/mass_mailing/wizard/test_mailing.py @@ -10,7 +10,7 @@ class TestMassMailing(models.TransientModel): email_to = fields.Char(string='Recipients', required=True, help='Comma-separated list of email addresses.', default=lambda self: self.env['mail.message']._get_default_from()) - mass_mailing_id = fields.Many2one('mail.mass_mailing', string='Mailing', required=True) + mass_mailing_id = fields.Many2one('mail.mass_mailing', string='Mailing', required=True, ondelete='cascade') @api.multi def send_mail_test(self): diff --git a/addons/mrp/models/mrp_bom.py b/addons/mrp/models/mrp_bom.py index 3feb7b498be..6ca6f8bbf4c 100644 --- a/addons/mrp/models/mrp_bom.py +++ b/addons/mrp/models/mrp_bom.py @@ -4,6 +4,7 @@ from odoo import api, fields, models, _ from odoo.addons import decimal_precision as dp from odoo.exceptions import UserError, ValidationError +from odoo.tools import float_round class MrpBom(models.Model): @@ -138,6 +139,10 @@ class MrpBom(models.Model): templates_done |= current_line.product_id.product_tmpl_id boms_done.append((bom, {'qty': converted_line_quantity, 'product': current_product, 'original_qty': quantity, 'parent_line': current_line})) else: + # We round up here because the user expects that if he has to consume a little more, the whole UOM unit + # should be consumed. + rounding = current_line.product_uom_id.rounding + line_quantity = float_round(line_quantity, precision_rounding=rounding, rounding_method='UP') lines_done.append((current_line, {'qty': line_quantity, 'product': current_product, 'original_qty': quantity, 'parent_line': parent_line})) return boms_done, lines_done 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/mrp_workorder.py b/addons/mrp/models/mrp_workorder.py index fc0da1ecdab..23cd851b780 100644 --- a/addons/mrp/models/mrp_workorder.py +++ b/addons/mrp/models/mrp_workorder.py @@ -6,7 +6,7 @@ from dateutil.relativedelta import relativedelta from odoo import api, fields, models, _ from odoo.exceptions import UserError -from odoo.tools import float_compare +from odoo.tools import float_compare, float_round from odoo.addons import decimal_precision as dp @@ -275,7 +275,8 @@ class MrpWorkorder(models.Model): raw_moves = self.move_raw_ids.filtered(lambda x: (x.has_tracking == 'none') and (x.state not in ('done', 'cancel')) and x.bom_line_id) for move in raw_moves: if move.unit_factor: - move.quantity_done += self.qty_producing * move.unit_factor + rounding = move.product_uom.rounding + move.quantity_done += float_round(self.qty_producing * move.unit_factor, precision_rounding=rounding) # Transfer quantities from temporary to final move lots or make them final for move_lot in self.active_move_lot_ids: diff --git a/addons/mrp/models/stock_move.py b/addons/mrp/models/stock_move.py index 9daa3069593..42df10850ca 100644 --- a/addons/mrp/models/stock_move.py +++ b/addons/mrp/models/stock_move.py @@ -3,7 +3,7 @@ from odoo import api, exceptions, fields, models, _ from odoo.exceptions import UserError -from odoo.tools import float_compare +from odoo.tools import float_compare, float_round from odoo.addons import decimal_precision as dp @@ -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') @@ -225,8 +225,14 @@ class StockMove(models.Model): moves_to_unreserve = self.env['stock.move'] # Create extra moves where necessary for move in moves: + # Here, the `quantity_done` was already rounded to the product UOM by the `do_produce` wizard. However, + # it is possible that the user changed the value before posting the inventory by a value that should be + # rounded according to the move's UOM. In this specific case, we chose to round up the value, because it + # is what is expected by the user (if i consumed/produced a little more, the whole UOM unit should be + # consumed/produced and the moves are split correctly). rounding = move.product_uom.rounding - if float_compare(move.quantity_done, 0.0, precision_rounding=rounding) <= 0: + move.quantity_done = float_round(move.quantity_done, precision_rounding=rounding, rounding_method ='UP') + if move.quantity_done <= 0: continue moves_todo |= move moves_todo |= move._create_extra_move() diff --git a/addons/mrp/tests/test_order.py b/addons/mrp/tests/test_order.py index 64a42fbb136..0623a892e52 100644 --- a/addons/mrp/tests/test_order.py +++ b/addons/mrp/tests/test_order.py @@ -427,3 +427,35 @@ class TestMrpOrder(TestMrpCommon): # check the consumed quants of the newly produced quant self.assertEquals(sum(second_move.quant_ids.mapped('consumed_quant_ids').mapped('qty')), 2) + + def test_rounding(self): + """ In previous versions we had rounding and efficiency fields. We check if we can still do the same, but with only the rounding on the UoM""" + self.product_6.uom_id.rounding = 1.0 + bom_eff = self.env['mrp.bom'].create({'product_id': self.product_6.id, + 'product_tmpl_id': self.product_6.product_tmpl_id.id, + 'product_qty': 1, + 'product_uom_id': self.product_6.uom_id.id, + 'type': 'normal', + 'bom_line_ids': [ + (0, 0, {'product_id': self.product_2.id, 'product_qty': 2.03}), + (0, 0, {'product_id': self.product_8.id, 'product_qty': 4.16}) + ]}) + production = self.env['mrp.production'].create({'name': 'MO efficiency test', + 'product_id': self.product_6.id, + 'product_qty': 20, + 'bom_id': bom_eff.id, + 'product_uom_id': self.product_6.uom_id.id,}) + #Check the production order has the right quantities + self.assertEqual(production.move_raw_ids[0].product_qty, 41, 'The quantity should be rounded up') + self.assertEqual(production.move_raw_ids[1].product_qty, 84, 'The quantity should be rounded up') + + # produce product + produce_wizard = self.env['mrp.product.produce'].with_context({ + 'active_id': production.id, + 'active_ids': [production.id], + }).create({ + 'product_qty': 8, + }) + produce_wizard.do_produce() + self.assertEqual(production.move_raw_ids[0].quantity_done, 16, 'Should use half-up rounding when producing') + self.assertEqual(production.move_raw_ids[1].quantity_done, 34, 'Should use half-up rounding when producing') diff --git a/addons/mrp/wizard/mrp_product_produce.py b/addons/mrp/wizard/mrp_product_produce.py index 3ac2b1bf7c3..b636fc15ec7 100644 --- a/addons/mrp/wizard/mrp_product_produce.py +++ b/addons/mrp/wizard/mrp_product_produce.py @@ -6,7 +6,7 @@ from datetime import datetime from odoo import api, fields, models, _ from odoo.addons import decimal_precision as dp from odoo.exceptions import UserError -from odoo.tools import float_compare +from odoo.tools import float_compare, float_round class MrpProductProduce(models.TransientModel): _name = "mrp.product.produce" @@ -80,13 +80,16 @@ class MrpProductProduce(models.TransientModel): raise UserError(_('You should at least produce some quantity')) for move in moves.filtered(lambda x: x.product_id.tracking == 'none' and x.state not in ('done', 'cancel')): if move.unit_factor: - move.quantity_done_store += quantity * move.unit_factor + rounding = move.product_uom.rounding + move.quantity_done_store += float_round(quantity * move.unit_factor, precision_rounding=rounding) moves = self.production_id.move_finished_ids.filtered(lambda x: x.product_id.tracking == 'none' and x.state not in ('done', 'cancel')) for move in moves: + rounding = move.product_uom.rounding if move.product_id.id == self.production_id.product_id.id: - move.quantity_done_store += quantity + move.quantity_done_store += float_round(quantity, precision_rounding=rounding) elif move.unit_factor: - move.quantity_done_store += quantity * move.unit_factor + # byproducts handling + move.quantity_done_store += float_round(quantity * move.unit_factor, precision_rounding=rounding) self.check_finished_move_lots() if self.production_id.state == 'confirmed': self.production_id.write({ 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 @@ - + diff --git a/addons/payment/models/account_payment.py b/addons/payment/models/account_payment.py index 8609b6b96c9..d507666f35f 100644 --- a/addons/payment/models/account_payment.py +++ b/addons/payment/models/account_payment.py @@ -41,7 +41,7 @@ class AccountPayment(models.Model): if self.payment_token_id.acquirer_id.auto_confirm == 'authorize': raise ValidationError('This feature is not available for payment acquirers set to the "Authorize" mode.\n' 'Please use a token from another provider than %s.' % self.payment_token_id.acquirer_id.name) - reference = "PAYMENT-%s-%s" % (self.id, datetime.datetime.now().strftime('%y%m%d_%H%M%S')) + reference = "P-%s-%s" % (self.id, datetime.datetime.now().strftime('%y%m%d_%H%M%S')) tx = self.env['payment.transaction'].create({ 'amount': self.amount, 'acquirer_id': self.payment_token_id.acquirer_id.id, diff --git a/addons/payment_authorize/models/payment.py b/addons/payment_authorize/models/payment.py index 8de18021bcd..2f2037dc95a 100644 --- a/addons/payment_authorize/models/payment.py +++ b/addons/payment_authorize/models/payment.py @@ -147,6 +147,17 @@ class TxAuthorize(models.Model): # FORM RELATED METHODS # -------------------------------------------------- + @api.model + def create(self, vals): + # The reference is used in the Authorize form to fill a field (invoiceNumber) which is + # limited to 20 characters. We truncate the reference now, since it will be reused at + # payment validation to find back the transaction. + if 'reference' in vals and 'acquirer_id' in vals: + acquier = self.env['payment.acquirer'].browse(vals['acquirer_id']) + if acquier.provider == 'authorize': + vals['reference'] = vals.get('reference', '')[:20] + return super(TxAuthorize, self).create(vals) + @api.model def _authorize_form_get_tx_from_data(self, data): """ Given a data dict coming from authorize, verify it and find the related diff --git a/addons/point_of_sale/models/pos_order.py b/addons/point_of_sale/models/pos_order.py index 7831bf2eb2c..ec96a147252 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"); + + $("",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"); - - $("",{ - 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/models.js b/addons/point_of_sale/static/src/js/models.js index bc4f2a2fd2e..80ea86b9be9 100644 --- a/addons/point_of_sale/static/src/js/models.js +++ b/addons/point_of_sale/static/src/js/models.js @@ -675,6 +675,8 @@ exports.PosModel = Backbone.Model.extend({ flushed.always(function(ids){ pushed.resolve(); }); + + return flushed; }); return pushed; }, @@ -775,6 +777,11 @@ exports.PosModel = Backbone.Model.extend({ var self = this; var timeout = typeof options.timeout === 'number' ? options.timeout : 7500 * orders.length; + // Keep the order ids that are about to be sent to the + // backend. In between create_from_ui and the success callback + // new orders may have been added to it. + var order_ids_to_sync = _.pluck(orders, 'id'); + // we try to send the order. shadow prevents a spinner if it takes too long. (unless we are sending an invoice, // then we want to notify the user that we are waiting on something ) var posOrderModel = new Model('pos.order'); @@ -789,8 +796,8 @@ exports.PosModel = Backbone.Model.extend({ timeout: timeout } ).then(function (server_ids) { - _.each(orders, function (order) { - self.db.remove_order(order.id); + _.each(order_ids_to_sync, function (order_id) { + self.db.remove_order(order_id); }); self.set('failed',false); return server_ids; 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 @@
Country -
+ +
@@ -1308,7 +1311,9 @@
  • Delete Paid Orders
  • Delete Unpaid Orders
  • Export Paid Orders
  • +
  • Download Paid Orders
  • Export Unpaid Orders
  • +
  • Download Unpaid Orders
  • Import Orders 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 @@ report.pos.order.pivot report.pos.order - + diff --git a/addons/product/models/product_uom.py b/addons/product/models/product_uom.py index 9c3baa4d6db..23acd5a5294 100644 --- a/addons/product/models/product_uom.py +++ b/addons/product/models/product_uom.py @@ -84,7 +84,7 @@ class ProductUoM(models.Model): if misc_category: values['category_id'] = misc_category.id else: - values['category_id'] = EnglishUoMCateg.name_create('Unsorted/Imported Units').id + values['category_id'] = EnglishUoMCateg.name_create('Unsorted/Imported Units')[0] new_uom = self.create(values) return new_uom.name_get()[0] 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 @@ + @@ -310,6 +311,7 @@ + diff --git a/addons/project/data/project_data.xml b/addons/project/data/project_data.xml index 4c83590bdd6..fe047401c22 100644 --- a/addons/project/data/project_data.xml +++ b/addons/project/data/project_data.xml @@ -97,7 +97,7 @@ - + Start here to discover Odoo followers @@ -111,25 +111,25 @@ - + 1 New - + 2 Basic - + 3 Advanced - + 1 0 @@ -141,7 +141,7 @@ - + 2 0 @@ -152,7 +152,7 @@ - + 3 0 @@ -163,7 +163,7 @@ - + 4 0 @@ -174,7 +174,7 @@ - + 0 @@ -184,7 +184,7 @@ - + Converse with your customers and colleagues project.task @@ -194,12 +194,12 @@ - + NeedAssistance - + 3 0 @@ -212,7 +212,7 @@ - + 4 0 @@ -224,7 +224,7 @@ - + 5 0 @@ -235,7 +235,7 @@ - + 0 @@ -245,7 +245,7 @@ Click on the bar in the upper right corner - + planner_icon.png planner_icon.png @@ -253,7 +253,7 @@ - + How to open the implementation guide ? project.task @@ -264,11 +264,11 @@ - + - + 0 @@ -278,7 +278,7 @@ Click on the gear icon on the column, to archive the stage, with all the tasks in it. You can also archive one card only, by clicking on the button in the task form view. - + 0 @@ -288,7 +288,7 @@ Click on the last column to create a stage. The name depends on the process. For example, in a customer service process, a stage name may be 'Backlog', 'Waiting Customer Feedback' or 'Done'. - + 0 @@ -298,7 +298,7 @@ - + 0 @@ -308,7 +308,7 @@ In the chatter, sending an email with an attachment will display the picture on the card. When there are several image attachments, you can choose which one you want to display. - + bird.jpg bird.jpg @@ -317,7 +317,7 @@ - + How to open the planner? project.task @@ -327,7 +327,7 @@ - + diff --git a/addons/project/data/web_planner_data.xml b/addons/project/data/web_planner_data.xml index 91bfecfb30d..8e8bf631798 100644 --- a/addons/project/data/web_planner_data.xml +++ b/addons/project/data/web_planner_data.xml @@ -124,7 +124,7 @@
    - + Tasks
    @@ -142,7 +142,7 @@
    - + Issues
    @@ -159,7 +159,7 @@
    - + Timesheets
    @@ -225,8 +225,8 @@
    - - + + @@ -246,8 +246,8 @@
    Need help with defining your Projects? Contact us now.
    - Website Live Chat - Email Our Project Expert + Website Live Chat + Email Our Project Expert
    @@ -360,8 +360,8 @@ @@ -379,7 +379,7 @@
    -
    +

    Define a Naming Convention
    Add keywords in the 'Task title' field, for example [Customer name] or [Website]. This will help you navigate and search through the dozens of tasks in your project. @@ -387,7 +387,7 @@

    -
    +

    Add a Deadline
    The deadline will help you determine if a task or issue is progressing as expected and to anticipate its next update. @@ -395,7 +395,7 @@

    -
    +

    Use Tags
    Tags are complementary to your project stages: they can work as a second level categorization which is very useful if you have a lot of Tasks or Issues to manage. Also, it becomes very easy to find Tasks or Issues by typing the Tag into the main Search bar. @@ -403,7 +403,7 @@

    -
    +

    Use Status Indicator
    The Status indicator helps you manage Tasks and Issues by giving them one of 3 different colour: Grey, Green or Red. The meaning of these Statuses can be freely configured, for example: @@ -414,7 +414,7 @@

    -
    +

    Set Priorities
    The is used to indicate priority: in Kanban views, high priority Tasks or Issues will be displayed on top. This is particulary useful if you use a Scrum methodology to indicate the tasks for the week. Similarly to Status, you can change the meaning of the Star indicator from the Project Stages tab. @@ -422,7 +422,7 @@

    -
    +

    Have Clear Responsibilities
    The person assigned to a task is responsible for its progress and their avatar is displayed in Kanban view for quick reference. Of course, the responsibility for a task can change depending on its stage. @@ -470,7 +470,7 @@

    - + Directly in Odoo
    @@ -488,7 +488,7 @@
    - + Via Chrome extension
    @@ -509,7 +509,7 @@
    - + From your Mobile phone
    @@ -570,9 +570,9 @@
    @@ -588,7 +588,7 @@
    - + Screen Customization
    @@ -607,7 +607,7 @@
    - + Workflow Customization
    @@ -624,7 +624,7 @@
    - + Create Custom Reports
    @@ -650,8 +650,8 @@
    @@ -671,7 +671,7 @@

    Create bills automatically based on Time & Material.

    - Install now + Install now
    @@ -680,7 +680,7 @@

    Organize your company, from personal tasks to collaborative meeting minutes.

    - Install now + Install now
    @@ -822,7 +822,7 @@

    The Right Way to use projects:

    The Wrong Way to use projects:

    The Right Way to use projects:

    The Wrong Way to use projects:

    - + exp. closing diff --git a/addons/stock/models/product.py b/addons/stock/models/product.py index 3aafae6aade..a661169296f 100644 --- a/addons/stock/models/product.py +++ b/addons/stock/models/product.py @@ -13,7 +13,7 @@ OPERATORS = { '>': py_operator.gt, '<=': py_operator.le, '>=': py_operator.ge, - '==': py_operator.eq, + '=': py_operator.eq, '!=': py_operator.ne } @@ -260,9 +260,6 @@ class Product(models.Model): if not isinstance(value, (float, int)): raise UserError('Invalid domain right operand') - if operator == '=': - operator = '==' - # TODO: Still optimization possible when searching virtual quantities ids = [] for product in self.search([]): @@ -272,7 +269,7 @@ class Product(models.Model): def _search_qty_available(self, operator, value): # TDE FIXME: should probably clean the search methods - if value == 0.0 and operator in ('==', '>=', '<='): + if value == 0.0 and operator in ('=', '>=', '<='): return self._search_product_quantity(operator, value, 'qty_available') product_ids = self._search_qty_available_new(operator, value, self._context.get('lot_id'), self._context.get('owner_id'), self._context.get('package_id')) return [('id', 'in', product_ids)] diff --git a/addons/stock/models/stock_move.py b/addons/stock/models/stock_move.py index 744aa1ff6df..d88ad3229f2 100644 --- a/addons/stock/models/stock_move.py +++ b/addons/stock/models/stock_move.py @@ -575,7 +575,9 @@ class StockMove(models.Model): # in case the move is returned, we want to try to find quants before forcing the assignment if not move.origin_returned_move_id: continue - if move.product_id.type == 'consu': + # if the move is preceeded, restrict the choice of quants in the ones moved previously in original move + ancestors = move.find_move_ancestors() + if move.product_id.type == 'consu' and not ancestors: moves_to_assign |= move continue else: @@ -584,8 +586,6 @@ class StockMove(models.Model): # we always search for yet unassigned quants main_domain[move.id] = [('reservation_id', '=', False), ('qty', '>', 0)] - # if the move is preceeded, restrict the choice of quants in the ones moved previously in original move - ancestors = move.find_move_ancestors() if move.state == 'waiting' and not ancestors: # if the waiting move hasn't yet any ancestor (PO/MO not confirmed yet), don't find any quant available in stock main_domain[move.id] += [('id', '=', False)] @@ -797,7 +797,11 @@ class StockMove(models.Model): # compute quantities for each lot + check quantities match lot_quantities = dict((pack_lot.lot_id.id, operation.product_uom_id._compute_quantity(pack_lot.qty, operation.product_id.uom_id) ) for pack_lot in operation.pack_lot_ids) - if operation.pack_lot_ids and float_compare(sum(lot_quantities.values()), operation.product_qty, precision_rounding=operation.product_uom_id.rounding) != 0.0: + + qty = operation.product_qty + if operation.product_uom_id and operation.product_uom_id != operation.product_id.uom_id: + qty = operation.product_uom_id._compute_quantity(qty, operation.product_id.uom_id) + if operation.pack_lot_ids and float_compare(sum(lot_quantities.values()), qty, precision_rounding=operation.product_id.uom_id.rounding) != 0.0: raise UserError(_('You have a difference between the quantity on the operation and the quantities specified for the lots. ')) quants_taken = [] @@ -972,4 +976,4 @@ class StockMove(models.Model): def quants_unreserve(self): self.filtered(lambda x: x.partially_available).write({'partially_available': False}) - self.mapped('reserved_quant_ids').sudo().write({'reservation_id': False}) \ No newline at end of file + self.mapped('reserved_quant_ids').sudo().write({'reservation_id': False}) 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/models/stock_picking.py b/addons/stock/models/stock_picking.py index 2a875453509..635e3b3ad53 100644 --- a/addons/stock/models/stock_picking.py +++ b/addons/stock/models/stock_picking.py @@ -316,22 +316,18 @@ class Picking(models.Model): self.state = 'cancel' elif all(move.state in ['cancel', 'done'] for move in self.move_lines): self.state = 'done' - elif self.move_type == 'one': - ordered_moves = self.move_lines.filtered( - lambda move: move.state not in ['cancel', 'done'] - ).sorted( - key=lambda move: (move.state == 'assigned' and 2) or (move.state == 'waiting' and 1) or 0, reverse=False - ) - self.state = ordered_moves[0].state else: - filtered_moves = self.move_lines.filtered(lambda move: move.state not in ['cancel', 'done']) - if not all(move.state == 'assigned' for move in filtered_moves) and any(move.state == 'assigned' for move in filtered_moves): - self.state = 'partially_available' - elif any(move.partially_available for move in filtered_moves): + # We sort our moves by importance of state: "confirmed" should be first, then we'll have + # "waiting" and finally "assigned" at the end. + moves_todo = self.move_lines\ + .filtered(lambda move: move.state not in ['cancel', 'done'])\ + .sorted(key=lambda move: (move.state == 'assigned' and 2) or (move.state == 'waiting' and 1) or 0) + if self.move_type == 'one': + self.state = moves_todo[0].state + elif moves_todo[0].state != 'assigned' and any(x.partially_available or x.state == 'assigned' for x in moves_todo): self.state = 'partially_available' else: - ordered_moves = filtered_moves.sorted(key=lambda move: (move.state == 'assigned' and 2) or (move.state == 'waiting' and 1) or 0, reverse=True) - self.state = ordered_moves[0].state + self.state = moves_todo[-1].state @api.one @api.depends('move_lines.priority') @@ -597,7 +593,10 @@ class Picking(models.Model): continue move_quants = move.reserved_quant_ids picking_quants += move_quants - forced_qty = (move.state == 'assigned') and move.product_qty - sum([x.qty for x in move_quants]) or 0 + forced_qty = 0.0 + if move.state == 'assigned': + qty = move.product_uom._compute_quantity(move.product_uom_qty, move.product_id.uom_id, round=False) + forced_qty = qty - sum([x.qty for x in move_quants]) # if we used force_assign() on the move, or if the move is incoming, forced_qty > 0 if float_compare(forced_qty, 0, precision_rounding=move.product_id.uom_id.rounding) > 0: if forced_qties.get(move.product_id): diff --git a/addons/stock/tests/test_stock_flow.py b/addons/stock/tests/test_stock_flow.py index 65d2a862866..3ee20467a88 100644 --- a/addons/stock/tests/test_stock_flow.py +++ b/addons/stock/tests/test_stock_flow.py @@ -1210,7 +1210,6 @@ class TestStockFlow(TestStockCommon): inventory5.action_done() self.assertEqual(inventory5_lines_count, inventory4_lines_count + 1, "The new product is not taken into account in the inventory valuation.") self.assertTrue(product.id in inventory5.line_ids.mapped('product_id').ids, "The new product is not take into account in the inventory valuation.") - def test_30_check_with_no_incoming_lot(self): """ Picking in without lots and picking out with""" @@ -1278,7 +1277,6 @@ class TestStockFlow(TestStockCommon): self.assertEqual(sum([x.qty for x in quants if x.lot_id.id == lot2.id]), 1.0, 'Wrong sum of quants with lot 2') self.assertEqual(sum([x.qty for x in quants if x.lot_id.id == lot3.id]), 2.0, 'Wrong sum of quants with lot 3') - def test_40_pack_in_pack(self): """ Put a pack in pack""" picking_out = self.PickingObj.create({ @@ -1368,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() @@ -1386,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() @@ -1536,3 +1536,194 @@ class TestStockFlow(TestStockCommon): # We should also make sure that when matching stock moves with pack operations, it takes the correct self.assertEqual(len(picking_out.move_lines[0].linked_move_operation_ids), 2, 'We should only have 2 links beween the move and the 2 operations') self.assertEqual(len(picking_out.move_lines[0].quant_ids), 2, 'We should have exactly 2 quants in the end') + + def test_70_picking_state_all_at_once_reserve(self): + """ This test will check that the state of the picking is correctly computed according + to the state of its move lines and its move type. + """ + # move_type: direct == partial, one == all at once + # picking: confirmed == waiting availability, partially_available = partially available + + # ----------------------------------------------------------- + # "all at once" and "reserve" scenario + # ----------------------------------------------------------- + # get one product in stock + inventory = self.env['stock.inventory'].create({ + 'name': 'Inventory Product Table', + 'filter': 'partial', + 'line_ids': [(0, 0, { + 'product_id': self.productA.id, + 'product_uom_id': self.productA.uom_id.id, + 'product_qty': 1, + 'location_id': self.stock_location + })] + }) + inventory.action_done() + + # create a "all at once" delivery order for two products + picking_out = self.PickingObj.create({ + 'partner_id': self.partner_agrolite_id, + 'picking_type_id': self.picking_type_out, + 'location_id': self.stock_location, + 'location_dest_id': self.customer_location}) + picking_out.move_type = 'one' + + self.MoveObj.create({ + 'name': self.productA.name, + 'product_id': self.productA.id, + 'product_uom_qty': 2, + 'product_uom': self.productA.uom_id.id, + 'picking_id': picking_out.id, + 'location_id': self.stock_location, + 'location_dest_id': self.customer_location}) + + # validate this delivery order, it should be in the waiting state + picking_out.action_assign() + self.assertEquals(picking_out.state, "confirmed") + + # receive one product in stock + inventory = self.env['stock.inventory'].create({ + 'name': 'Inventory Product Table', + 'filter': 'partial', + 'line_ids': [(0, 0, { + 'product_id': self.productA.id, + 'product_uom_id': self.productA.uom_id.id, + 'product_qty': 2, + 'location_id': self.stock_location + })] + }) + inventory.action_done() + + # recheck availability of the delivery order, it should be assigned + picking_out.action_assign() + self.assertEquals(picking_out.state, "assigned") + + def test_71_picking_state_all_at_once_force_assign(self): + """ This test will check that the state of the picking is correctly computed according + to the state of its move lines and its move type. + """ + # move_type: direct == partial, one == all at once + # picking: confirmed == waiting availability, partially_available = partially available + + # ----------------------------------------------------------- + # "all at once" and "force assign" scenario + # ----------------------------------------------------------- + # create a "all at once" delivery order for two products + picking_out = self.PickingObj.create({ + 'partner_id': self.partner_agrolite_id, + 'picking_type_id': self.picking_type_out, + 'location_id': self.stock_location, + 'location_dest_id': self.customer_location}) + picking_out.move_type = 'direct' + + self.MoveObj.create({ + 'name': self.productA.name, + 'product_id': self.productA.id, + 'product_uom_qty': 2, + 'product_uom': self.productA.uom_id.id, + 'picking_id': picking_out.id, + 'location_id': self.stock_location, + 'location_dest_id': self.customer_location}) + + # validate this delivery order, it should be in the waiting state + picking_out.action_assign() + self.assertEquals(picking_out.state, "confirmed") + + # force assign on the delivery order, it should be assigned + picking_out.force_assign() + self.assertEquals(picking_out.state, "assigned") + + def test_72_picking_state_partial_reserve(self): + """ This test will check that the state of the picking is correctly computed according + to the state of its move lines and its move type. + """ + # move_type: direct == partial, one == all at once + # picking: confirmed == waiting availability, partially_available = partially available + + # ----------------------------------------------------------- + # "partial" and "reserve" scenario + # ----------------------------------------------------------- + # get one product in stock + inventory = self.env['stock.inventory'].create({ + 'name': 'Inventory Product Table', + 'filter': 'partial', + 'line_ids': [(0, 0, { + 'product_id': self.productA.id, + 'product_uom_id': self.productA.uom_id.id, + 'product_qty': 1, + 'location_id': self.stock_location + })] + }) + inventory.action_done() + + # create a "partial" delivery order for two products + picking_out = self.PickingObj.create({ + 'partner_id': self.partner_agrolite_id, + 'picking_type_id': self.picking_type_out, + 'location_id': self.stock_location, + 'location_dest_id': self.customer_location}) + picking_out.move_type = 'direct' + + self.MoveObj.create({ + 'name': self.productA.name, + 'product_id': self.productA.id, + 'product_uom_qty': 2, + 'product_uom': self.productA.uom_id.id, + 'picking_id': picking_out.id, + 'location_id': self.stock_location, + 'location_dest_id': self.customer_location}) + + # validate this delivery order, it should be in partially available + picking_out.action_assign() + self.assertEquals(picking_out.state, "partially_available") + + # receive one product in stock + inventory = self.env['stock.inventory'].create({ + 'name': 'Inventory Product Table', + 'filter': 'partial', + 'line_ids': [(0, 0, { + 'product_id': self.productA.id, + 'product_uom_id': self.productA.uom_id.id, + 'product_qty': 2, + 'location_id': self.stock_location + })] + }) + inventory.action_done() + + # recheck availability of the delivery order, it should be assigned + picking_out.action_assign() + self.assertEquals(picking_out.state, "assigned") + + def test_73_picking_state_partial_force_assign(self): + """ This test will check that the state of the picking is correctly computed according + to the state of its move lines and its move type. + """ + # move_type: direct == partial, one == all at once + # picking: confirmed == waiting availability, partially_available = partially available + + # ----------------------------------------------------------- + # "partial" and "force assign" scenario + # ----------------------------------------------------------- + picking_out = self.PickingObj.create({ + 'partner_id': self.partner_agrolite_id, + 'picking_type_id': self.picking_type_out, + 'location_id': self.stock_location, + 'location_dest_id': self.customer_location}) + picking_out.move_type = 'direct' + + self.MoveObj.create({ + 'name': self.productA.name, + 'product_id': self.productA.id, + 'product_uom_qty': 2, + 'product_uom': self.productA.uom_id.id, + 'picking_id': picking_out.id, + 'location_id': self.stock_location, + 'location_dest_id': self.customer_location}) + + # validate this delivery order, it should be in the waiting state + picking_out.action_assign() + self.assertEquals(picking_out.state, "confirmed") + + # force assign on the delivery order, it should be assigned + picking_out.force_assign() + self.assertEquals(picking_out.state, "assigned") \ No newline at end of file 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 @@ - - - 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 @@
    -
    +