class AuthOAuthProvider(models.Model): """Class defining the configuration values of an OAuth2 provider""" _name = 'auth.oauth.provider' _description = 'OAuth2 provider' _order = 'name' name = fields.Char(string='Provider name', required=True) # Name of the OAuth2 entity, Google, etc client_id = fields.Char(string='Client ID') # Our identifier auth_endpoint = fields.Char( string='Authentication URL', required=True) # OAuth provider URL to authenticate users scope = fields.Char() # OAUth user data desired to access validation_endpoint = fields.Char( string='Validation URL', required=True) # OAuth provider URL to validate tokens data_endpoint = fields.Char(string='Data URL') enabled = fields.Boolean(string='Allowed') css_class = fields.Char(string='CSS class', default='zocial') body = fields.Char(required=True) sequence = fields.Integer()
class ChallengeLine(models.Model): """Gamification challenge line Predefined goal for 'gamification_challenge' These are generic list of goals with only the target goal defined Should only be created for the gamification.challenge object """ _name = 'gamification.challenge.line' _description = 'Gamification generic goal for challenge' _order = "sequence, id" challenge_id = fields.Many2one('gamification.challenge', string='Challenge', required=True, ondelete="cascade") definition_id = fields.Many2one('gamification.goal.definition', string='Goal Definition', required=True, ondelete="cascade") sequence = fields.Integer('Sequence', help='Sequence number for ordering', default=1) target_goal = fields.Float('Target Value to Reach', required=True) name = fields.Char("Name", related='definition_id.name') condition = fields.Selection("Condition", related='definition_id.condition', readonly=True) definition_suffix = fields.Char("Unit", related='definition_id.suffix', readonly=True) definition_monetary = fields.Boolean("Monetary", related='definition_id.monetary', readonly=True) definition_full_suffix = fields.Char("Suffix", related='definition_id.full_suffix', readonly=True)
class HrEmployee(models.Model): _inherit = "hr.employee" goal_ids = fields.One2many('gamification.goal', string='Employee HR Goals', compute='_compute_employee_goals') badge_ids = fields.One2many( 'gamification.badge.user', string='Employee Badges', compute='_compute_employee_badges', help= "All employee badges, linked to the employee either directly or through the user" ) has_badges = fields.Boolean(compute='_compute_employee_badges') # necessary for correct dependencies of badge_ids and has_badges direct_badge_ids = fields.One2many( 'gamification.badge.user', 'employee_id', help="Badges directly linked to the employee") @api.depends('user_id.goal_ids.challenge_id.category') def _compute_employee_goals(self): for employee in self: employee.goal_ids = self.env['gamification.goal'].search([ ('user_id', '=', employee.user_id.id), ('challenge_id.category', '=', 'hr'), ]) @api.depends('direct_badge_ids', 'user_id.badge_ids.employee_id') def _compute_employee_badges(self): for employee in self: badge_ids = self.env['gamification.badge.user'].search([ '|', ('employee_id', '=', employee.id), '&', ('employee_id', '=', False), ('user_id', '=', employee.user_id.id) ]) employee.has_badges = bool(badge_ids) employee.badge_ids = badge_ids
class AccountJournal(models.Model): _inherit = 'account.journal' journal_user = fields.Boolean('Use in Point of Sale', help="Check this box if this journal define a payment method that can be used in a point of sale.") amount_authorized_diff = fields.Float('Amount Authorized Difference', help="This field depicts the maximum difference allowed between the ending balance and the theoretical cash when " "closing a session, for non-POS managers. If this maximum is reached, the user will have an error message at " "the closing of his session saying that he needs to contact his manager.") @api.model def search(self, args, offset=0, limit=None, order=None, count=False): session_id = self.env.context.get('pos_session_id', False) if session_id: session = self.env['pos.session'].browse(session_id) if session: args += [('id', 'in', session.config_id.journal_ids.ids)] return super(AccountJournal, self).search(args=args, offset=offset, limit=limit, order=order, count=count) @api.onchange('type') def onchange_type(self): if self.type not in ['bank', 'cash']: self.journal_user = False
class Forum(models.Model): _name = 'forum.forum' _description = 'Forum' _inherit = ['mail.thread', 'website.seo.metadata', 'website.published.mixin'] @api.model_cr def init(self): """ Add forum uuid for user email validation. TDE TODO: move me somewhere else, auto_init ? """ forum_uuids = self.env['ir.config_parameter'].search([('key', '=', 'website_forum.uuid')]) if not forum_uuids: forum_uuids.set_param('website_forum.uuid', str(uuid.uuid4())) @api.model def _get_default_faq(self): with misc.file_open('website_forum/data/forum_default_faq.html', 'r') as f: return f.read() def _default_website(self): default_website_id = self.env.ref('website.default_website') return [default_website_id.id] if default_website_id else None # description and use name = fields.Char('Forum Name', required=True, translate=True) active = fields.Boolean(default=True) faq = fields.Html('Guidelines', default=_get_default_faq, translate=True) description = fields.Text( 'Description', translate=True, default=lambda s: _('This community is for professionals and enthusiasts of our products and services. ' 'Share and discuss the best content and new marketing ideas, ' 'build your professional profile and become a better marketer together.')) welcome_message = fields.Html( 'Welcome Message', default = """<section class="bg-info" style="height: 168px;"><div class="container"> <div class="row"> <div class="col-md-12"> <h1 class="text-center" style="text-align: left;">Welcome!</h1> <p class="text-muted text-center" style="text-align: left;">This community is for professionals and enthusiasts of our products and services. Share and discuss the best content and new marketing ideas, build your professional profile and become a better marketer together.</p> </div> <div class="col-md-12"> <a href="#" class="js_close_intro">Hide Intro</a> <a class="btn btn-primary forum_register_url" href="/web/login">Register</a> </div> </div> </div> </section>""") default_order = fields.Selection([ ('create_date desc', 'Newest'), ('write_date desc', 'Last Updated'), ('vote_count desc', 'Most Voted'), ('relevancy desc', 'Relevance'), ('child_count desc', 'Answered')], string='Default Order', required=True, default='write_date desc') relevancy_post_vote = fields.Float('First Relevance Parameter', default=0.8, help="This formula is used in order to sort by relevance. The variable 'votes' represents number of votes for a post, and 'days' is number of days since the post creation") relevancy_time_decay = fields.Float('Second Relevance Parameter', default=1.8) default_post_type = fields.Selection([ ('question', 'Question'), ('discussion', 'Discussion'), ('link', 'Link')], string='Default Post', required=True, default='question') allow_question = fields.Boolean('Questions', help="Users can answer only once per question. Contributors can edit answers and mark the right ones.", default=True) allow_discussion = fields.Boolean('Discussions', default=True) allow_link = fields.Boolean('Links', help="When clicking on the post, it redirects to an external link", default=True) allow_bump = fields.Boolean('Allow Bump', default=True, help='Check this box to display a popup for posts older than 10 days ' 'without any given answer. The popup will offer to share it on social ' 'networks. When shared, a question is bumped at the top of the forum.') allow_share = fields.Boolean('Sharing Options', default=True, help='After posting the user will be proposed to share its question ' 'or answer on social networks, enabling social network propagation ' 'of the forum content.') count_posts_waiting_validation = fields.Integer(string="Number of posts waiting for validation", compute='_compute_count_posts_waiting_validation') count_flagged_posts = fields.Integer(string='Number of flagged posts', compute='_compute_count_flagged_posts') # karma generation karma_gen_question_new = fields.Integer(string='Asking a question', default=2) karma_gen_question_upvote = fields.Integer(string='Question upvoted', default=5) karma_gen_question_downvote = fields.Integer(string='Question downvoted', default=-2) karma_gen_answer_upvote = fields.Integer(string='Answer upvoted', default=10) karma_gen_answer_downvote = fields.Integer(string='Answer downvoted', default=-2) karma_gen_answer_accept = fields.Integer(string='Accepting an answer', default=2) karma_gen_answer_accepted = fields.Integer(string='Answer accepted', default=15) karma_gen_answer_flagged = fields.Integer(string='Answer flagged', default=-100) # karma-based actions karma_ask = fields.Integer(string='Ask questions', default=3) karma_answer = fields.Integer(string='Answer questions', default=3) karma_edit_own = fields.Integer(string='Edit own posts', default=1) karma_edit_all = fields.Integer(string='Edit all posts', default=300) karma_edit_retag = fields.Integer(string='Change question tags', default=75, oldname="karma_retag") karma_close_own = fields.Integer(string='Close own posts', default=100) karma_close_all = fields.Integer(string='Close all posts', default=500) karma_unlink_own = fields.Integer(string='Delete own posts', default=500) karma_unlink_all = fields.Integer(string='Delete all posts', default=1000) karma_tag_create = fields.Integer(string='Create new tags', default=30) karma_upvote = fields.Integer(string='Upvote', default=5) karma_downvote = fields.Integer(string='Downvote', default=50) karma_answer_accept_own = fields.Integer(string='Accept an answer on own questions', default=20) karma_answer_accept_all = fields.Integer(string='Accept an answer to all questions', default=500) karma_comment_own = fields.Integer(string='Comment own posts', default=1) karma_comment_all = fields.Integer(string='Comment all posts', default=1) karma_comment_convert_own = fields.Integer(string='Convert own answers to comments and vice versa', default=50) karma_comment_convert_all = fields.Integer(string='Convert all answers to comments and vice versa', default=500) karma_comment_unlink_own = fields.Integer(string='Unlink own comments', default=50) karma_comment_unlink_all = fields.Integer(string='Unlink all comments', default=500) karma_flag = fields.Integer(string='Flag a post as offensive', default=500) karma_dofollow = fields.Integer(string='Nofollow links', help='If the author has not enough karma, a nofollow attribute is added to links', default=500) karma_editor = fields.Integer(string='Editor Features: image and links', default=30, oldname='karma_editor_link_files') karma_user_bio = fields.Integer(string='Display detailed user biography', default=750) karma_post = fields.Integer(string='Ask questions without validation', default=100) karma_moderate = fields.Integer(string='Moderate posts', default=1000) website_ids = fields.Many2many('website', 'website_forum_pub_rel', 'website_id', 'forum_id', string='Websites', copy=False, default=_default_website, help='List of websites in which ' 'Forum will published.') @api.one @api.constrains('allow_question', 'allow_discussion', 'allow_link', 'default_post_type') def _check_default_post_type(self): if (self.default_post_type == 'question' and not self.allow_question) \ or (self.default_post_type == 'discussion' and not self.allow_discussion) \ or (self.default_post_type == 'link' and not self.allow_link): raise ValidationError(_('You cannot choose %s as default post since the forum does not allow it.') % self.default_post_type) @api.one def _compute_count_posts_waiting_validation(self): domain = [('forum_id', '=', self.id), ('state', '=', 'pending')] self.count_posts_waiting_validation = self.env['forum.post'].search_count(domain) @api.one def _compute_count_flagged_posts(self): domain = [('forum_id', '=', self.id), ('state', '=', 'flagged')] self.count_flagged_posts = self.env['forum.post'].search_count(domain) @api.model def create(self, values): return super(Forum, self.with_context(mail_create_nolog=True, mail_create_nosubscribe=True)).create(values) @api.multi def write(self, vals): res = super(Forum, self).write(vals) if 'active' in vals: # archiving/unarchiving a forum does it on its posts, too self.env['forum.post'].with_context(active_test=False).search([('forum_id', 'in', self.ids)]).write({'active': vals['active']}) return res @api.model def _tag_to_write_vals(self, tags=''): Tag = self.env['forum.tag'] post_tags = [] existing_keep = [] user = self.env.user for tag in (tag for tag in tags.split(',') if tag): if tag.startswith('_'): # it's a new tag # check that not arleady created meanwhile or maybe excluded by the limit on the search tag_ids = Tag.search([('name', '=', tag[1:])]) if tag_ids: existing_keep.append(int(tag_ids[0])) else: # check if user have Karma needed to create need tag if user.exists() and user.karma >= self.karma_tag_create and len(tag) and len(tag[1:].strip()): post_tags.append((0, 0, {'name': tag[1:], 'forum_id': self.id})) else: existing_keep.append(int(tag)) post_tags.insert(0, [6, 0, existing_keep]) return post_tags def get_tags_first_char(self): """ get set of first letter of forum tags """ tags = self.env['forum.tag'].search([('forum_id', '=', self.id), ('posts_count', '>', 0)]) return sorted(set([tag.name[0].upper() for tag in tags if len(tag.name)]))
class AccountAssetAsset(models.Model): _name = 'account.asset.asset' _description = 'Asset/Revenue Recognition' _inherit = ['mail.thread'] entry_count = fields.Integer(compute='_entry_count', string='# Asset Entries') name = fields.Char(string='Asset Name', required=True, readonly=True, states={'draft': [('readonly', False)]}) code = fields.Char(string='Reference', size=32, readonly=True, states={'draft': [('readonly', False)]}) value = fields.Float(string='Gross Value', required=True, readonly=True, digits=0, states={'draft': [('readonly', False)]}, oldname='purchase_value') currency_id = fields.Many2one('res.currency', string='Currency', required=True, readonly=True, states={'draft': [('readonly', False)]}, default=lambda self: self.env.user.company_id.currency_id.id) company_id = fields.Many2one('res.company', string='Company', required=True, readonly=True, states={'draft': [('readonly', False)]}, default=lambda self: self.env['res.company']._company_default_get('account.asset.asset')) note = fields.Text() category_id = fields.Many2one('account.asset.category', string='Category', required=True, change_default=True, readonly=True, states={'draft': [('readonly', False)]}) date = fields.Date(string='Date', required=True, readonly=True, states={'draft': [('readonly', False)]}, default=fields.Date.context_today, oldname="purchase_date") state = fields.Selection([('draft', 'Draft'), ('open', 'Running'), ('close', 'Close')], 'Status', required=True, copy=False, default='draft', help="When an asset is created, the status is 'Draft'.\n" "If the asset is confirmed, the status goes in 'Running' and the depreciation lines can be posted in the accounting.\n" "You can manually close an asset when the depreciation is over. If the last line of depreciation is posted, the asset automatically goes in that status.") active = fields.Boolean(default=True) partner_id = fields.Many2one('res.partner', string='Partner', readonly=True, states={'draft': [('readonly', False)]}) method = fields.Selection([('linear', 'Linear'), ('degressive', 'Degressive')], string='Computation Method', required=True, readonly=True, states={'draft': [('readonly', False)]}, default='linear', help="Choose the method to use to compute the amount of depreciation lines.\n * Linear: Calculated on basis of: Gross Value / Number of Depreciations\n" " * Degressive: Calculated on basis of: Residual Value * Degressive Factor") method_number = fields.Integer(string='Number of Depreciations', readonly=True, states={'draft': [('readonly', False)]}, default=5, help="The number of depreciations needed to depreciate your asset") method_period = fields.Integer(string='Number of Months in a Period', required=True, readonly=True, default=12, states={'draft': [('readonly', False)]}, help="The amount of time between two depreciations, in months") method_end = fields.Date(string='Ending Date', readonly=True, states={'draft': [('readonly', False)]}) method_progress_factor = fields.Float(string='Degressive Factor', readonly=True, default=0.3, states={'draft': [('readonly', False)]}) value_residual = fields.Float(compute='_amount_residual', method=True, digits=0, string='Residual Value') method_time = fields.Selection([('number', 'Number of Entries'), ('end', 'Ending Date')], string='Time Method', required=True, readonly=True, default='number', states={'draft': [('readonly', False)]}, help="Choose the method to use to compute the dates and number of entries.\n" " * Number of Entries: Fix the number of entries and the time between 2 depreciations.\n" " * Ending Date: Choose the time between 2 depreciations and the date the depreciations won't go beyond.") prorata = fields.Boolean(string='Prorata Temporis', readonly=True, states={'draft': [('readonly', False)]}, help='Indicates that the first depreciation entry for this asset have to be done from the purchase date instead of the first January / Start date of fiscal year') depreciation_line_ids = fields.One2many('account.asset.depreciation.line', 'asset_id', string='Depreciation Lines', readonly=True, states={'draft': [('readonly', False)], 'open': [('readonly', False)]}) salvage_value = fields.Float(string='Salvage Value', digits=0, readonly=True, states={'draft': [('readonly', False)]}, help="It is the amount you plan to have that you cannot depreciate.") invoice_id = fields.Many2one('account.invoice', string='Invoice', states={'draft': [('readonly', False)]}, copy=False) type = fields.Selection(related="category_id.type", string='Type', required=True) @api.multi def unlink(self): for asset in self: if asset.state in ['open', 'close']: raise UserError(_('You cannot delete a document is in %s state.') % (asset.state,)) for depreciation_line in asset.depreciation_line_ids: if depreciation_line.move_id: raise UserError(_('You cannot delete a document that contains posted entries.')) return super(AccountAssetAsset, self).unlink() @api.multi def _get_last_depreciation_date(self): """ @param id: ids of a account.asset.asset objects @return: Returns a dictionary of the effective dates of the last depreciation entry made for given asset ids. If there isn't any, return the purchase date of this asset """ self.env.cr.execute(""" SELECT a.id as id, COALESCE(MAX(m.date),a.date) AS date FROM account_asset_asset a LEFT JOIN account_asset_depreciation_line rel ON (rel.asset_id = a.id) LEFT JOIN account_move m ON (rel.move_id = m.id) WHERE a.id IN %s GROUP BY a.id, m.date """, (tuple(self.ids),)) result = dict(self.env.cr.fetchall()) return result @api.model def _cron_generate_entries(self): self.compute_generated_entries(datetime.today()) @api.model def compute_generated_entries(self, date, asset_type=None): # Entries generated : one by grouped category and one by asset from ungrouped category created_move_ids = [] type_domain = [] if asset_type: type_domain = [('type', '=', asset_type)] ungrouped_assets = self.env['account.asset.asset'].search(type_domain + [('state', '=', 'open'), ('category_id.group_entries', '=', False)]) created_move_ids += ungrouped_assets._compute_entries(date, group_entries=False) for grouped_category in self.env['account.asset.category'].search(type_domain + [('group_entries', '=', True)]): assets = self.env['account.asset.asset'].search([('state', '=', 'open'), ('category_id', '=', grouped_category.id)]) created_move_ids += assets._compute_entries(date, group_entries=True) return created_move_ids def _compute_board_amount(self, sequence, residual_amount, amount_to_depr, undone_dotation_number, posted_depreciation_line_ids, total_days, depreciation_date): amount = 0 if sequence == undone_dotation_number: amount = residual_amount else: if self.method == 'linear': amount = amount_to_depr / (undone_dotation_number - len(posted_depreciation_line_ids)) if self.prorata: amount = amount_to_depr / self.method_number if sequence == 1: if self.method_period % 12 != 0: date = datetime.strptime(self.date, '%Y-%m-%d') month_days = calendar.monthrange(date.year, date.month)[1] days = month_days - date.day + 1 amount = (amount_to_depr / self.method_number) / month_days * days else: days = (self.company_id.compute_fiscalyear_dates(depreciation_date)['date_to'] - depreciation_date).days + 1 amount = (amount_to_depr / self.method_number) / total_days * days elif self.method == 'degressive': amount = residual_amount * self.method_progress_factor if self.prorata: if sequence == 1: if self.method_period % 12 != 0: date = datetime.strptime(self.date, '%Y-%m-%d') month_days = calendar.monthrange(date.year, date.month)[1] days = month_days - date.day + 1 amount = (residual_amount * self.method_progress_factor) / month_days * days else: days = (self.company_id.compute_fiscalyear_dates(depreciation_date)['date_to'] - depreciation_date).days + 1 amount = (residual_amount * self.method_progress_factor) / total_days * days return amount def _compute_board_undone_dotation_nb(self, depreciation_date, total_days): undone_dotation_number = self.method_number if self.method_time == 'end': end_date = datetime.strptime(self.method_end, DF).date() undone_dotation_number = 0 while depreciation_date <= end_date: depreciation_date = date(depreciation_date.year, depreciation_date.month, depreciation_date.day) + relativedelta(months=+self.method_period) undone_dotation_number += 1 if self.prorata: undone_dotation_number += 1 return undone_dotation_number @api.multi def compute_depreciation_board(self): self.ensure_one() posted_depreciation_line_ids = self.depreciation_line_ids.filtered(lambda x: x.move_check).sorted(key=lambda l: l.depreciation_date) unposted_depreciation_line_ids = self.depreciation_line_ids.filtered(lambda x: not x.move_check) # Remove old unposted depreciation lines. We cannot use unlink() with One2many field commands = [(2, line_id.id, False) for line_id in unposted_depreciation_line_ids] if self.value_residual != 0.0: amount_to_depr = residual_amount = self.value_residual if self.prorata: # if we already have some previous validated entries, starting date is last entry + method perio if posted_depreciation_line_ids and posted_depreciation_line_ids[-1].depreciation_date: last_depreciation_date = datetime.strptime(posted_depreciation_line_ids[-1].depreciation_date, DF).date() depreciation_date = last_depreciation_date + relativedelta(months=+self.method_period) else: depreciation_date = datetime.strptime(self._get_last_depreciation_date()[self.id], DF).date() else: # depreciation_date = 1st of January of purchase year if annual valuation, 1st of # purchase month in other cases if self.method_period >= 12: asset_date = datetime.strptime(self.date[:4] + '-01-01', DF).date() else: asset_date = datetime.strptime(self.date[:7] + '-01', DF).date() # if we already have some previous validated entries, starting date isn't 1st January but last entry + method period if posted_depreciation_line_ids and posted_depreciation_line_ids[-1].depreciation_date: last_depreciation_date = datetime.strptime(posted_depreciation_line_ids[-1].depreciation_date, DF).date() depreciation_date = last_depreciation_date + relativedelta(months=+self.method_period) else: depreciation_date = asset_date day = depreciation_date.day month = depreciation_date.month year = depreciation_date.year total_days = (year % 4) and 365 or 366 undone_dotation_number = self._compute_board_undone_dotation_nb(depreciation_date, total_days) for x in range(len(posted_depreciation_line_ids), undone_dotation_number): sequence = x + 1 amount = self._compute_board_amount(sequence, residual_amount, amount_to_depr, undone_dotation_number, posted_depreciation_line_ids, total_days, depreciation_date) amount = self.currency_id.round(amount) if float_is_zero(amount, precision_rounding=self.currency_id.rounding): continue residual_amount -= amount vals = { 'amount': amount, 'asset_id': self.id, 'sequence': sequence, 'name': (self.code or '') + '/' + str(sequence), 'remaining_value': residual_amount, 'depreciated_value': self.value - (self.salvage_value + residual_amount), 'depreciation_date': depreciation_date.strftime(DF), } commands.append((0, False, vals)) # Considering Depr. Period as months depreciation_date = date(year, month, day) + relativedelta(months=+self.method_period) day = depreciation_date.day month = depreciation_date.month year = depreciation_date.year self.write({'depreciation_line_ids': commands}) return True @api.multi def validate(self): self.write({'state': 'open'}) fields = [ 'method', 'method_number', 'method_period', 'method_end', 'method_progress_factor', 'method_time', 'salvage_value', 'invoice_id', ] ref_tracked_fields = self.env['account.asset.asset'].fields_get(fields) for asset in self: tracked_fields = ref_tracked_fields.copy() if asset.method == 'linear': del(tracked_fields['method_progress_factor']) if asset.method_time != 'end': del(tracked_fields['method_end']) else: del(tracked_fields['method_number']) dummy, tracking_value_ids = asset._message_track(tracked_fields, dict.fromkeys(fields)) asset.message_post(subject=_('Asset created'), tracking_value_ids=tracking_value_ids) def _get_disposal_moves(self): move_ids = [] for asset in self: unposted_depreciation_line_ids = asset.depreciation_line_ids.filtered(lambda x: not x.move_check) if unposted_depreciation_line_ids: old_values = { 'method_end': asset.method_end, 'method_number': asset.method_number, } # Remove all unposted depr. lines commands = [(2, line_id.id, False) for line_id in unposted_depreciation_line_ids] # Create a new depr. line with the residual amount and post it sequence = len(asset.depreciation_line_ids) - len(unposted_depreciation_line_ids) + 1 today = datetime.today().strftime(DF) vals = { 'amount': asset.value_residual, 'asset_id': asset.id, 'sequence': sequence, 'name': (asset.code or '') + '/' + str(sequence), 'remaining_value': 0, 'depreciated_value': asset.value - asset.salvage_value, # the asset is completely depreciated 'depreciation_date': today, } commands.append((0, False, vals)) asset.write({'depreciation_line_ids': commands, 'method_end': today, 'method_number': sequence}) tracked_fields = self.env['account.asset.asset'].fields_get(['method_number', 'method_end']) changes, tracking_value_ids = asset._message_track(tracked_fields, old_values) if changes: asset.message_post(subject=_('Asset sold or disposed. Accounting entry awaiting for validation.'), tracking_value_ids=tracking_value_ids) move_ids += asset.depreciation_line_ids[-1].create_move(post_move=False) return move_ids @api.multi def set_to_close(self): move_ids = self._get_disposal_moves() if move_ids: name = _('Disposal Move') view_mode = 'form' if len(move_ids) > 1: name = _('Disposal Moves') view_mode = 'tree,form' return { 'name': name, 'view_type': 'form', 'view_mode': view_mode, 'res_model': 'account.move', 'type': 'ir.actions.act_window', 'target': 'current', 'res_id': move_ids[0], } @api.multi def set_to_draft(self): self.write({'state': 'draft'}) @api.one @api.depends('value', 'salvage_value', 'depreciation_line_ids.move_check', 'depreciation_line_ids.amount') def _amount_residual(self): total_amount = 0.0 for line in self.depreciation_line_ids: if line.move_check: total_amount += line.amount self.value_residual = self.value - total_amount - self.salvage_value @api.onchange('company_id') def onchange_company_id(self): self.currency_id = self.company_id.currency_id.id @api.multi @api.depends('depreciation_line_ids.move_id') def _entry_count(self): for asset in self: res = self.env['account.asset.depreciation.line'].search_count([('asset_id', '=', asset.id), ('move_id', '!=', False)]) asset.entry_count = res or 0 @api.one @api.constrains('prorata', 'method_time') def _check_prorata(self): if self.prorata and self.method_time != 'number': raise ValidationError(_('Prorata temporis can be applied only for time method "number of depreciations".')) @api.onchange('category_id') def onchange_category_id(self): vals = self.onchange_category_id_values(self.category_id.id) # We cannot use 'write' on an object that doesn't exist yet if vals: for k, v in vals['value'].items(): setattr(self, k, v) def onchange_category_id_values(self, category_id): if category_id: category = self.env['account.asset.category'].browse(category_id) return { 'value': { 'method': category.method, 'method_number': category.method_number, 'method_time': category.method_time, 'method_period': category.method_period, 'method_progress_factor': category.method_progress_factor, 'method_end': category.method_end, 'prorata': category.prorata, } } @api.onchange('method_time') def onchange_method_time(self): if self.method_time != 'number': self.prorata = False @api.multi def copy_data(self, default=None): if default is None: default = {} default['name'] = self.name + _(' (copy)') return super(AccountAssetAsset, self).copy_data(default) @api.multi def _compute_entries(self, date, group_entries=False): depreciation_ids = self.env['account.asset.depreciation.line'].search([ ('asset_id', 'in', self.ids), ('depreciation_date', '<=', date), ('move_check', '=', False)]) if group_entries: return depreciation_ids.create_grouped_move() return depreciation_ids.create_move() @api.model def create(self, vals): asset = super(AccountAssetAsset, self.with_context(mail_create_nolog=True)).create(vals) asset.compute_depreciation_board() return asset @api.multi def write(self, vals): res = super(AccountAssetAsset, self).write(vals) if 'depreciation_line_ids' not in vals and 'state' not in vals: for rec in self: rec.compute_depreciation_board() return res @api.multi def open_entries(self): move_ids = [] for asset in self: for depreciation_line in asset.depreciation_line_ids: if depreciation_line.move_id: move_ids.append(depreciation_line.move_id.id) return { 'name': _('Journal Entries'), 'view_type': 'form', 'view_mode': 'tree,form', 'res_model': 'account.move', 'view_id': False, 'type': 'ir.actions.act_window', 'domain': [('id', 'in', move_ids)], }
class ResCompany(models.Model): _inherit = 'res.company' vat_check_vies = fields.Boolean(string='Verify VAT Numbers')
class MaintenanceEquipment(models.Model): _name = 'maintenance.equipment' _inherit = ['mail.thread', 'mail.activity.mixin'] _description = 'Equipment' @api.multi def _track_subtype(self, init_values): self.ensure_one() if 'owner_user_id' in init_values and self.owner_user_id: return 'maintenance.mt_mat_assign' return super(MaintenanceEquipment, self)._track_subtype(init_values) @api.multi def name_get(self): result = [] for record in self: if record.name and record.serial_no: result.append((record.id, record.name + '/' + record.serial_no)) if record.name and not record.serial_no: result.append((record.id, record.name)) return result @api.model def name_search(self, name, args=None, operator='ilike', limit=100): args = args or [] recs = self.browse() if name: recs = self.search([('name', '=', name)] + args, limit=limit) if not recs: recs = self.search([('name', operator, name)] + args, limit=limit) return recs.name_get() name = fields.Char('Equipment Name', required=True, translate=True) active = fields.Boolean(default=True) technician_user_id = fields.Many2one('res.users', string='Technician', track_visibility='onchange', oldname='user_id') owner_user_id = fields.Many2one('res.users', string='Owner', track_visibility='onchange') category_id = fields.Many2one('maintenance.equipment.category', string='Equipment Category', track_visibility='onchange', group_expand='_read_group_category_ids') partner_id = fields.Many2one('res.partner', string='Vendor', domain="[('supplier', '=', 1)]") partner_ref = fields.Char('Vendor Reference') location = fields.Char('Location') model = fields.Char('Model') serial_no = fields.Char('Serial Number', copy=False) assign_date = fields.Date('Assigned Date', track_visibility='onchange') cost = fields.Float('Cost') note = fields.Text('Note') warranty = fields.Date('Warranty') color = fields.Integer('Color Index') scrap_date = fields.Date('Scrap Date') maintenance_ids = fields.One2many('maintenance.request', 'equipment_id') maintenance_count = fields.Integer(compute='_compute_maintenance_count', string="Maintenance", store=True) maintenance_open_count = fields.Integer(compute='_compute_maintenance_count', string="Current Maintenance", store=True) period = fields.Integer('Days between each preventive maintenance') next_action_date = fields.Date(compute='_compute_next_maintenance', string='Date of the next preventive maintenance', store=True) maintenance_team_id = fields.Many2one('maintenance.team', string='Maintenance Team') maintenance_duration = fields.Float(help="Maintenance Duration in hours.") @api.depends('period', 'maintenance_ids.request_date', 'maintenance_ids.close_date') def _compute_next_maintenance(self): 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): self.maintenance_count = len(self.maintenance_ids) self.maintenance_open_count = len(self.maintenance_ids.filtered(lambda x: not x.stage_id.done)) @api.onchange('category_id') def _onchange_category_id(self): self.technician_user_id = self.category_id.technician_user_id _sql_constraints = [ ('serial_no', 'unique(serial_no)', "Another asset already exists with this serial number!"), ] @api.model def create(self, vals): equipment = super(MaintenanceEquipment, self).create(vals) if equipment.owner_user_id: equipment.message_subscribe_users(user_ids=[equipment.owner_user_id.id]) return equipment @api.multi def write(self, vals): if vals.get('owner_user_id'): self.message_subscribe_users(user_ids=[vals['owner_user_id']]) return super(MaintenanceEquipment, self).write(vals) @api.model def _message_get_auto_subscribe_fields(self, updated_fields, auto_follow_fields=None): """ mail.thread override so user_id which has no special access allowance is not automatically subscribed. """ if auto_follow_fields is None: auto_follow_fields = [] return super(MaintenanceEquipment, self)._message_get_auto_subscribe_fields(updated_fields, auto_follow_fields) @api.model def _read_group_category_ids(self, categories, domain, order): """ Read group customization in order to display all the categories in the kanban view, even if they are empty. """ 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, 'maintenance_team_id': self.maintenance_team_id.id, 'duration': self.maintenance_duration, }) @api.model def _cron_generate_requests(self): """ 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 MaintenanceEquipmentCategory(models.Model): _name = 'maintenance.equipment.category' _inherit = ['mail.alias.mixin', 'mail.thread'] _description = 'Asset Category' @api.one @api.depends('equipment_ids') def _compute_fold(self): self.fold = False if self.equipment_count else True name = fields.Char('Category Name', required=True, translate=True) technician_user_id = fields.Many2one('res.users', 'Responsible', track_visibility='onchange', default=lambda self: self.env.uid, oldname='user_id') color = fields.Integer('Color Index') note = fields.Text('Comments', translate=True) equipment_ids = fields.One2many('maintenance.equipment', 'category_id', string='Equipments', copy=False) equipment_count = fields.Integer(string="Equipment", compute='_compute_equipment_count') maintenance_ids = fields.One2many('maintenance.request', 'category_id', copy=False) maintenance_count = fields.Integer(string="Maintenance", compute='_compute_maintenance_count') alias_id = fields.Many2one( 'mail.alias', 'Alias', ondelete='restrict', required=True, help="Email alias for this equipment category. New emails will automatically " "create new maintenance request for this equipment category.") fold = fields.Boolean(string='Folded in Maintenance Pipe', compute='_compute_fold', store=True) @api.multi def _compute_equipment_count(self): equipment_data = self.env['maintenance.equipment'].read_group([('category_id', 'in', self.ids)], ['category_id'], ['category_id']) mapped_data = dict([(m['category_id'][0], m['category_id_count']) for m in equipment_data]) for category in self: category.equipment_count = mapped_data.get(category.id, 0) @api.multi def _compute_maintenance_count(self): maintenance_data = self.env['maintenance.request'].read_group([('category_id', 'in', self.ids)], ['category_id'], ['category_id']) mapped_data = dict([(m['category_id'][0], m['category_id_count']) for m in maintenance_data]) for category in self: category.maintenance_count = mapped_data.get(category.id, 0) @api.model def create(self, vals): self = self.with_context(alias_model_name='maintenance.request', alias_parent_model_name=self._name) if not vals.get('alias_name'): vals['alias_name'] = vals.get('name') category_id = super(MaintenanceEquipmentCategory, self).create(vals) category_id.alias_id.write({'alias_parent_thread_id': category_id.id, 'alias_defaults': {'category_id': category_id.id}}) return category_id @api.multi def unlink(self): MailAlias = self.env['mail.alias'] for category in self: if category.equipment_ids or category.maintenance_ids: raise UserError(_("You cannot delete an equipment category containing equipments or maintenance requests.")) MailAlias += category.alias_id res = super(MaintenanceEquipmentCategory, self).unlink() MailAlias.unlink() return res def get_alias_model_name(self, vals): return vals.get('alias_model', 'maintenance.equipment') def get_alias_values(self): values = super(MaintenanceEquipmentCategory, self).get_alias_values() values['alias_defaults'] = {'category_id': self.id} return values
class MrpProduction(models.Model): """ Manufacturing Orders """ _name = 'mrp.production' _description = 'Manufacturing Order' _date_name = 'date_planned_start' _inherit = ['mail.thread', 'mail.activity.mixin'] _order = 'date_planned_start asc,id' @api.model def _get_default_picking_type(self): return self.env['stock.picking.type'].search( [('code', '=', 'mrp_operation'), ('warehouse_id.company_id', 'in', [ self.env.context.get('company_id', self.env.user.company_id.id), False ])], limit=1).id @api.model def _get_default_location_src_id(self): location = False if self._context.get('default_picking_type_id'): location = self.env['stock.picking.type'].browse( self.env.context['default_picking_type_id'] ).default_location_src_id if not location: location = self.env.ref('stock.stock_location_stock', raise_if_not_found=False) return location and location.id or False @api.model def _get_default_location_dest_id(self): location = False if self._context.get('default_picking_type_id'): location = self.env['stock.picking.type'].browse( self.env.context['default_picking_type_id'] ).default_location_dest_id if not location: location = self.env.ref('stock.stock_location_stock', raise_if_not_found=False) return location and location.id or False name = fields.Char('Reference', copy=False, readonly=True, default=lambda x: _('New')) origin = fields.Char( 'Source', copy=False, help= "Reference of the document that generated this production order request." ) product_id = fields.Many2one('product.product', 'Product', domain=[('type', 'in', ['product', 'consu'])], readonly=True, required=True, states={'confirmed': [('readonly', False)]}) product_tmpl_id = fields.Many2one('product.template', 'Product Template', related='product_id.product_tmpl_id') product_qty = fields.Float( 'Quantity To Produce', default=1.0, digits=dp.get_precision('Product Unit of Measure'), readonly=True, required=True, track_visibility='onchange', states={'confirmed': [('readonly', False)]}) product_uom_id = fields.Many2one( 'product.uom', 'Product Unit of Measure', oldname='product_uom', readonly=True, required=True, states={'confirmed': [('readonly', False)]}) picking_type_id = fields.Many2one('stock.picking.type', 'Operation Type', default=_get_default_picking_type, required=True) location_src_id = fields.Many2one( 'stock.location', 'Raw Materials Location', default=_get_default_location_src_id, readonly=True, required=True, states={'confirmed': [('readonly', False)]}, help="Location where the system will look for components.") location_dest_id = fields.Many2one( 'stock.location', 'Finished Products Location', default=_get_default_location_dest_id, readonly=True, required=True, states={'confirmed': [('readonly', False)]}, help="Location where the system will stock the finished products.") date_planned_start = fields.Datetime( 'Deadline Start', copy=False, default=fields.Datetime.now, index=True, required=True, states={'confirmed': [('readonly', False)]}, oldname="date_planned") date_planned_finished = fields.Datetime( 'Deadline End', copy=False, default=fields.Datetime.now, index=True, states={'confirmed': [('readonly', False)]}) date_start = fields.Datetime('Start Date', copy=False, index=True, readonly=True) date_finished = fields.Datetime('End Date', copy=False, index=True, readonly=True) bom_id = fields.Many2one( 'mrp.bom', 'Bill of Material', readonly=True, states={'confirmed': [('readonly', False)]}, help= "Bill of Materials allow you to define the list of required raw materials to make a finished product." ) routing_id = fields.Many2one( 'mrp.routing', 'Routing', readonly=True, compute='_compute_routing', store=True, help= "The list of operations (list of work centers) to produce the finished product. The routing " "is mainly used to compute work center costs during operations and to plan future loads on " "work centers based on production planning.") move_raw_ids = fields.One2many('stock.move', 'raw_material_production_id', 'Raw Materials', oldname='move_lines', copy=False, states={ 'done': [('readonly', True)], 'cancel': [('readonly', True)] }, domain=[('scrapped', '=', False)]) move_finished_ids = fields.One2many('stock.move', 'production_id', 'Finished Products', copy=False, states={ 'done': [('readonly', True)], 'cancel': [('readonly', True)] }, domain=[('scrapped', '=', False)]) finished_move_line_ids = fields.One2many('stock.move.line', compute='_compute_lines', inverse='_inverse_lines', string="Finished Product") workorder_ids = fields.One2many('mrp.workorder', 'production_id', 'Work Orders', copy=False, oldname='workcenter_lines', readonly=True) workorder_count = fields.Integer('# Work Orders', compute='_compute_workorder_count') workorder_done_count = fields.Integer( '# Done Work Orders', compute='_compute_workorder_done_count') move_dest_ids = fields.One2many('stock.move', 'created_production_id', string="Stock Movements of Produced Goods") state = fields.Selection([('confirmed', 'Confirmed'), ('planned', 'Planned'), ('progress', 'In Progress'), ('done', 'Done'), ('cancel', 'Cancelled')], string='State', copy=False, default='confirmed', track_visibility='onchange') availability = fields.Selection( [('assigned', 'Available'), ('partially_available', 'Partially Available'), ('waiting', 'Waiting'), ('none', 'None')], string='Materials Availability', compute='_compute_availability', store=True) unreserve_visible = fields.Boolean( 'Allowed to Unreserve Inventory', compute='_compute_unreserve_visible', help='Technical field to check when we can unreserve') post_visible = fields.Boolean( 'Allowed to Post Inventory', compute='_compute_post_visible', help='Technical field to check when we can post') consumed_less_than_planned = fields.Boolean( compute='_compute_consumed_less_than_planned', help= 'Technical field used to see if we have to display a warning or not when confirming an order.' ) user_id = fields.Many2one('res.users', 'Responsible', default=lambda self: self._uid) company_id = fields.Many2one('res.company', 'Company', default=lambda self: self.env['res.company']. _company_default_get('mrp.production'), required=True) check_to_done = fields.Boolean( compute="_get_produced_qty", string="Check Produced Qty", help="Technical Field to see if we can show 'Mark as Done' button") qty_produced = fields.Float(compute="_get_produced_qty", string="Quantity Produced") procurement_group_id = fields.Many2one('procurement.group', 'Procurement Group', copy=False) propagate = fields.Boolean( 'Propagate cancel and split', help= 'If checked, when the previous move of the move (which was generated by a next procurement) is cancelled or split, the move generated by this move will too' ) has_moves = fields.Boolean(compute='_has_moves') scrap_ids = fields.One2many('stock.scrap', 'production_id', 'Scraps') scrap_count = fields.Integer(compute='_compute_scrap_move_count', string='Scrap Move') priority = fields.Selection([('0', 'Not urgent'), ('1', 'Normal'), ('2', 'Urgent'), ('3', 'Very Urgent')], 'Priority', readonly=True, states={'confirmed': [('readonly', False)]}, default='1') is_locked = fields.Boolean('Is Locked', default=True, copy=False) show_final_lots = fields.Boolean('Show Final Lots', compute='_compute_show_lots') production_location_id = fields.Many2one( 'stock.location', "Production Location", related='product_id.property_stock_production') @api.depends('product_id.tracking') def _compute_show_lots(self): for production in self: production.show_final_lots = production.product_id.tracking != 'none' def _inverse_lines(self): """ Little hack to make sure that when you change something on these objects, it gets saved""" pass @api.depends('move_finished_ids.move_line_ids') def _compute_lines(self): for production in self: production.finished_move_line_ids = production.move_finished_ids.mapped( 'move_line_ids') @api.multi @api.depends('bom_id.routing_id', 'bom_id.routing_id.operation_ids') def _compute_routing(self): for production in self: if production.bom_id.routing_id.operation_ids: production.routing_id = production.bom_id.routing_id.id else: production.routing_id = False @api.multi @api.depends('workorder_ids') def _compute_workorder_count(self): data = self.env['mrp.workorder'].read_group( [('production_id', 'in', self.ids)], ['production_id'], ['production_id']) count_data = dict( (item['production_id'][0], item['production_id_count']) for item in data) for production in self: production.workorder_count = count_data.get(production.id, 0) @api.multi @api.depends('workorder_ids.state') def _compute_workorder_done_count(self): data = self.env['mrp.workorder'].read_group( [('production_id', 'in', self.ids), ('state', '=', 'done')], ['production_id'], ['production_id']) count_data = dict( (item['production_id'][0], item['production_id_count']) for item in data) for production in self: production.workorder_done_count = count_data.get(production.id, 0) @api.multi @api.depends('move_raw_ids.state', 'workorder_ids.move_raw_ids', 'bom_id.ready_to_produce') def _compute_availability(self): for order in self: if not order.move_raw_ids: order.availability = 'none' continue if order.bom_id.ready_to_produce == 'all_available': order.availability = any( move.state not in ('assigned', 'done', 'cancel') for move in order.move_raw_ids) and 'waiting' or 'assigned' else: move_raw_ids = order.move_raw_ids.filtered( lambda m: m.product_qty) partial_list = [ x.state in ('partially_available', 'assigned') for x in move_raw_ids ] assigned_list = [ x.state in ('assigned', 'done', 'cancel') for x in move_raw_ids ] order.availability = (all(assigned_list) and 'assigned') or ( any(partial_list) and 'partially_available') or 'waiting' @api.depends('move_raw_ids', 'is_locked', 'state', 'move_raw_ids.quantity_done') def _compute_unreserve_visible(self): for order in self: already_reserved = order.is_locked and order.state not in ( 'done', 'cancel') and order.mapped('move_raw_ids.move_line_ids') any_quantity_done = any( [m.quantity_done > 0 for m in order.move_raw_ids]) order.unreserve_visible = not any_quantity_done and already_reserved @api.multi @api.depends('move_raw_ids.quantity_done', 'move_finished_ids.quantity_done', 'is_locked') def _compute_post_visible(self): for order in self: if order.product_tmpl_id._is_cost_method_standard(): order.post_visible = order.is_locked and any( (x.quantity_done > 0 and x.state not in ['done', 'cancel']) for x in order.move_raw_ids | order.move_finished_ids) else: order.post_visible = order.is_locked and any( (x.quantity_done > 0 and x.state not in ['done', 'cancel']) for x in order.move_finished_ids) @api.multi @api.depends('move_raw_ids.quantity_done', 'move_raw_ids.product_uom_qty') def _compute_consumed_less_than_planned(self): for order in self: order.consumed_less_than_planned = any( order.move_raw_ids.filtered(lambda move: float_compare( move.quantity_done, move.product_uom_qty, precision_rounding=move.product_uom.rounding) == -1)) @api.multi @api.depends('workorder_ids.state', 'move_finished_ids', 'is_locked') def _get_produced_qty(self): for production in self: done_moves = production.move_finished_ids.filtered( lambda x: x.state != 'cancel' and x.product_id.id == production .product_id.id) qty_produced = sum(done_moves.mapped('quantity_done')) wo_done = True if any([ x.state not in ('done', 'cancel') for x in production.workorder_ids ]): wo_done = False production.check_to_done = production.is_locked and done_moves and ( qty_produced >= production.product_qty) and ( production.state not in ('done', 'cancel')) and wo_done production.qty_produced = qty_produced return True @api.multi @api.depends('move_raw_ids') def _has_moves(self): for mo in self: mo.has_moves = any(mo.move_raw_ids) @api.multi def _compute_scrap_move_count(self): data = self.env['stock.scrap'].read_group( [('production_id', 'in', self.ids)], ['production_id'], ['production_id']) count_data = dict( (item['production_id'][0], item['production_id_count']) for item in data) for production in self: production.scrap_count = count_data.get(production.id, 0) _sql_constraints = [ ('name_uniq', 'unique(name, company_id)', 'Reference must be unique per Company!'), ('qty_positive', 'check (product_qty > 0)', 'The quantity to produce must be positive!'), ] @api.onchange('product_id', 'picking_type_id', 'company_id') def onchange_product_id(self): """ Finds UoM of changed product. """ if not self.product_id: self.bom_id = False else: bom = self.env['mrp.bom']._bom_find( product=self.product_id, picking_type=self.picking_type_id, company_id=self.company_id.id) if bom.type == 'normal': self.bom_id = bom.id else: self.bom_id = False self.product_uom_id = self.product_id.uom_id.id return { 'domain': { 'product_uom_id': [('category_id', '=', self.product_id.uom_id.category_id.id)] } } @api.onchange('picking_type_id') def onchange_picking_type(self): location = self.env.ref('stock.stock_location_stock') self.location_src_id = self.picking_type_id.default_location_src_id.id or location.id self.location_dest_id = self.picking_type_id.default_location_dest_id.id or location.id @api.multi def write(self, vals): res = super(MrpProduction, self).write(vals) if 'date_planned_start' in vals: moves = (self.mapped('move_raw_ids') + self.mapped('move_finished_ids') ).filtered(lambda r: r.state not in ['done', 'cancel']) moves.write({ 'date_expected': vals['date_planned_start'], }) return res @api.model def create(self, values): if not values.get('name', False) or values['name'] == _('New'): if values.get('picking_type_id'): values['name'] = self.env['stock.picking.type'].browse( values['picking_type_id']).sequence_id.next_by_id() else: values['name'] = self.env['ir.sequence'].next_by_code( 'mrp.production') or _('New') if not values.get('procurement_group_id'): values['procurement_group_id'] = self.env[ "procurement.group"].create({ 'name': values['name'] }).id production = super(MrpProduction, self).create(values) production._generate_moves() return production @api.multi def unlink(self): if any(production.state != 'cancel' for production in self): raise UserError( _('Cannot delete a manufacturing order not in cancel state')) return super(MrpProduction, self).unlink() def action_toggle_is_locked(self): self.ensure_one() self.is_locked = not self.is_locked return True @api.multi def _generate_moves(self): for production in self: production._generate_finished_moves() factor = production.product_uom_id._compute_quantity( production.product_qty, production.bom_id.product_uom_id ) / production.bom_id.product_qty boms, lines = production.bom_id.explode( production.product_id, factor, picking_type=production.bom_id.picking_type_id) production._generate_raw_moves(lines) # Check for all draft moves whether they are mto or not production._adjust_procure_method() production.move_raw_ids._action_confirm() return True def _generate_finished_moves(self): move = self.env['stock.move'].create({ 'name': self.name, 'date': self.date_planned_start, 'date_expected': self.date_planned_start, 'product_id': self.product_id.id, 'product_uom': self.product_uom_id.id, 'product_uom_qty': self.product_qty, 'location_id': self.product_id.property_stock_production.id, 'location_dest_id': self.location_dest_id.id, 'company_id': self.company_id.id, 'production_id': self.id, 'origin': self.name, 'group_id': self.procurement_group_id.id, 'propagate': self.propagate, 'move_dest_ids': [(4, x.id) for x in self.move_dest_ids], }) move._action_confirm() return move def _generate_raw_moves(self, exploded_lines): self.ensure_one() moves = self.env['stock.move'] for bom_line, line_data in exploded_lines: moves += self._generate_raw_move(bom_line, line_data) return moves def _generate_raw_move(self, bom_line, line_data): quantity = line_data['qty'] # alt_op needed for the case when you explode phantom bom and all the lines will be consumed in the operation given by the parent bom line alt_op = line_data['parent_line'] and line_data[ 'parent_line'].operation_id.id or False if bom_line.child_bom_id and bom_line.child_bom_id.type == 'phantom': return self.env['stock.move'] if bom_line.product_id.type not in ['product', 'consu']: return self.env['stock.move'] if self.routing_id: routing = self.routing_id else: routing = self.bom_id.routing_id if routing and routing.location_id: source_location = routing.location_id else: source_location = self.location_src_id original_quantity = self.product_qty - self.qty_produced data = { 'sequence': bom_line.sequence, '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, 'product_uom': bom_line.product_uom_id.id, 'location_id': source_location.id, 'location_dest_id': self.product_id.property_stock_production.id, 'raw_material_production_id': self.id, 'company_id': self.company_id.id, 'operation_id': bom_line.operation_id.id or alt_op, 'price_unit': bom_line.product_id.standard_price, 'procure_method': 'make_to_stock', 'origin': self.name, 'warehouse_id': source_location.get_warehouse().id, 'group_id': self.procurement_group_id.id, 'propagate': self.propagate, 'unit_factor': quantity / original_quantity, } return self.env['stock.move'].create(data) @api.multi def _adjust_procure_method(self): try: mto_route = self.env['stock.warehouse']._get_mto_route() except: mto_route = False for move in self.move_raw_ids: product = move.product_id routes = product.route_ids + product.route_from_categ_ids # TODO: optimize with read_group? pull = self.env['procurement.rule'].search( [('route_id', 'in', [x.id for x in routes]), ('location_src_id', '=', move.location_id.id), ('location_id', '=', move.location_dest_id.id)], limit=1) if pull and (pull.procure_method == 'make_to_order'): move.procure_method = pull.procure_method elif not pull: # If there is no make_to_stock rule either if mto_route and mto_route.id in [x.id for x in routes]: move.procure_method = 'make_to_order' @api.multi def _update_raw_move(self, bom_line, line_data): quantity = line_data['qty'] self.ensure_one() move = self.move_raw_ids.filtered( lambda x: x.bom_line_id.id == bom_line.id and x.state not in ('done', 'cancel')) if move: if quantity > 0: move[0].write({'product_uom_qty': quantity}) elif quantity < 0: # Do not remove 0 lines if move[0].quantity_done > 0: raise UserError( _('Lines need to be deleted, but can not as you still have some quantities to consume in them. ' )) move[0]._action_cancel() move[0].unlink() return move else: self._generate_raw_move(bom_line, line_data) @api.multi def action_assign(self): for production in self: production.move_raw_ids._action_assign() return True @api.multi def open_produce_product(self): self.ensure_one() action = self.env.ref('mrp.act_mrp_product_produce').read()[0] return action @api.multi def button_plan(self): """ Create work orders. And probably do stuff, like things. """ orders_to_plan = self.filtered( lambda order: order.routing_id and order.state == 'confirmed') for order in orders_to_plan: quantity = order.product_uom_id._compute_quantity( order.product_qty, order.bom_id.product_uom_id) / order.bom_id.product_qty boms, lines = order.bom_id.explode( order.product_id, quantity, picking_type=order.bom_id.picking_type_id) order._generate_workorders(boms) return orders_to_plan.write({'state': 'planned'}) @api.multi def _generate_workorders(self, exploded_boms): workorders = self.env['mrp.workorder'] for bom, bom_data in exploded_boms: # If the routing of the parent BoM and phantom BoM are the same, don't recreate work orders, but use one master routing if bom.routing_id.id and ( not bom_data['parent_line'] or bom_data['parent_line'].bom_id.routing_id.id != bom.routing_id.id): workorders += self._workorders_create(bom, bom_data) return workorders def _workorders_create(self, bom, bom_data): """ :param bom: in case of recursive boms: we could create work orders for child BoMs """ workorders = self.env['mrp.workorder'] bom_qty = bom_data['qty'] # Initial qty producing if self.product_id.tracking == 'serial': quantity = 1.0 else: quantity = self.product_qty - sum( self.move_finished_ids.mapped('quantity_done')) quantity = quantity if (quantity > 0) else 0 for operation in bom.routing_id.operation_ids: # create workorder cycle_number = math.ceil( bom_qty / operation.workcenter_id.capacity) # TODO: float_round UP duration_expected = (operation.workcenter_id.time_start + operation.workcenter_id.time_stop + cycle_number * operation.time_cycle * 100.0 / operation.workcenter_id.time_efficiency) workorder = workorders.create({ 'name': operation.name, 'production_id': self.id, 'workcenter_id': operation.workcenter_id.id, 'operation_id': operation.id, 'duration_expected': duration_expected, 'state': len(workorders) == 0 and 'ready' or 'pending', 'qty_producing': quantity, 'capacity': operation.workcenter_id.capacity, }) if workorders: workorders[-1].next_work_order_id = workorder.id workorders += workorder # assign moves; last operation receive all unassigned moves (which case ?) moves_raw = self.move_raw_ids.filtered( lambda move: move.operation_id == operation) if len(workorders) == len(bom.routing_id.operation_ids): moves_raw |= self.move_raw_ids.filtered( lambda move: not move.operation_id) moves_finished = self.move_finished_ids.filtered( lambda move: move.operation_id == operation ) #TODO: code does nothing, unless maybe by_products? moves_raw.mapped('move_line_ids').write( {'workorder_id': workorder.id}) (moves_finished + moves_raw).write({'workorder_id': workorder.id}) workorder._generate_lot_ids() return workorders @api.multi def action_cancel(self): """ Cancels production order, unfinished stock moves and set procurement orders in exception """ if any(workorder.state == 'progress' for workorder in self.mapped('workorder_ids')): raise UserError( _('You can not cancel production order, a work order is still in progress.' )) for production in self: production.workorder_ids.filtered( lambda x: x.state != 'cancel').action_cancel() finish_moves = production.move_finished_ids.filtered( lambda x: x.state not in ('done', 'cancel')) raw_moves = production.move_raw_ids.filtered( lambda x: x.state not in ('done', 'cancel')) (finish_moves | raw_moves)._action_cancel() self.write({'state': 'cancel', 'is_locked': True}) return True def _cal_price(self, consumed_moves): self.ensure_one() return True @api.multi def post_inventory(self): for order in self: moves_not_to_do = order.move_raw_ids.filtered( lambda x: x.state == 'done') moves_to_do = order.move_raw_ids.filtered(lambda x: x.state not in ('done', 'cancel')) for move in moves_to_do.filtered( lambda m: m.product_qty == 0.0 and m.quantity_done > 0): move.product_uom_qty = move.quantity_done moves_to_do._action_done() moves_to_do = order.move_raw_ids.filtered( lambda x: x.state == 'done') - moves_not_to_do order._cal_price(moves_to_do) moves_to_finish = order.move_finished_ids.filtered( lambda x: x.state not in ('done', 'cancel')) moves_to_finish._action_done() #order.action_assign() consume_move_lines = moves_to_do.mapped('active_move_line_ids') for moveline in moves_to_finish.mapped('active_move_line_ids'): if moveline.product_id == order.product_id and moveline.move_id.has_tracking != 'none': if any( [not ml.lot_produced_id for ml in consume_move_lines]): raise UserError( _('You can not consume without telling for which lot you consumed it' )) # Link all movelines in the consumed with same lot_produced_id false or the correct lot_produced_id filtered_lines = consume_move_lines.filtered( lambda x: x.lot_produced_id == moveline.lot_id) moveline.write({ 'consume_line_ids': [(6, 0, [x for x in filtered_lines.ids])] }) else: # Link with everything moveline.write({ 'consume_line_ids': [(6, 0, [x for x in consume_move_lines.ids])] }) return True @api.multi def button_mark_done(self): self.ensure_one() for wo in self.workorder_ids: if wo.time_ids.filtered(lambda x: (not x.date_end) and ( x.loss_type in ('productive', 'performance'))): raise UserError(_('Work order %s is still running') % wo.name) self.post_inventory() moves_to_cancel = (self.move_raw_ids | self.move_finished_ids).filtered( lambda x: x.state not in ('done', 'cancel')) moves_to_cancel._action_cancel() self.write({'state': 'done', 'date_finished': fields.Datetime.now()}) return self.write({'state': 'done'}) @api.multi def do_unreserve(self): for production in self: production.move_raw_ids.filtered( lambda x: x.state not in ('done', 'cancel'))._do_unreserve() return True @api.multi def button_unreserve(self): self.ensure_one() self.do_unreserve() return True @api.multi def button_scrap(self): self.ensure_one() return { 'name': _('Scrap'), 'view_type': 'form', 'view_mode': 'form', 'res_model': 'stock.scrap', 'view_id': self.env.ref('stock.stock_scrap_form_view2').id, 'type': 'ir.actions.act_window', 'context': { 'default_production_id': self.id, 'product_ids': (self.move_raw_ids.filtered(lambda x: x.state not in ('done', 'cancel')) | self.move_finished_ids.filtered(lambda x: x.state == 'done') ).mapped('product_id').ids, }, 'target': 'new', } @api.multi def action_see_move_scrap(self): self.ensure_one() action = self.env.ref('stock.action_stock_scrap').read()[0] action['domain'] = [('production_id', '=', self.id)] return action
class IrUiMenu(models.Model): _name = 'ir.ui.menu' _order = "sequence,id" _parent_store = True def __init__(self, *args, **kwargs): super(IrUiMenu, self).__init__(*args, **kwargs) self.pool['ir.model.access'].register_cache_clearing_method(self._name, 'clear_caches') name = fields.Char(string='Menu', required=True, translate=True) active = fields.Boolean(default=True) sequence = fields.Integer(default=10) child_id = fields.One2many('ir.ui.menu', 'parent_id', string='Child IDs') parent_id = fields.Many2one('ir.ui.menu', string='Parent Menu', index=True, ondelete="restrict") parent_left = fields.Integer(index=True) parent_right = fields.Integer(index=True) groups_id = fields.Many2many('res.groups', 'ir_ui_menu_group_rel', 'menu_id', 'gid', string='Groups', help="If you have groups, the visibility of this menu will be based on these groups. "\ "If this field is empty, actpy will compute visibility based on the related object's read access.") complete_name = fields.Char(compute='_compute_complete_name', string='Full Path') web_icon = fields.Char(string='Web Icon File') action = fields.Reference(selection=[('ir.actions.report', 'ir.actions.report'), ('ir.actions.act_window', 'ir.actions.act_window'), ('ir.actions.act_url', 'ir.actions.act_url'), ('ir.actions.server', 'ir.actions.server'), ('ir.actions.client', 'ir.actions.client')]) web_icon_data = fields.Binary(string='Web Icon Image', attachment=True) @api.depends('name', 'parent_id.complete_name') def _compute_complete_name(self): for menu in self: menu.complete_name = menu._get_full_name() def _get_full_name(self, level=6): """ Return the full name of ``self`` (up to a certain level). """ if level <= 0: return '...' if self.parent_id: return self.parent_id._get_full_name(level - 1) + MENU_ITEM_SEPARATOR + (self.name or "") else: return self.name def read_image(self, path): if not path: return False path_info = path.split(',') icon_path = get_module_resource(path_info[0], path_info[1]) icon_image = False if icon_path: with tools.file_open(icon_path, 'rb') as icon_file: icon_image = base64.encodestring(icon_file.read()) return icon_image @api.constrains('parent_id') def _check_parent_id(self): if not self._check_recursion(): raise ValidationError(_('Error! You cannot create recursive menus.')) @api.model @tools.ormcache('frozenset(self.env.user.groups_id.ids)', 'debug') def _visible_menu_ids(self, debug=False): """ Return the ids of the menu items visible to the user. """ # retrieve all menus, and determine which ones are visible context = {'ir.ui.menu.full_list': True} menus = self.with_context(context).search([]) groups = self.env.user.groups_id if not debug: groups = groups - self.env.ref('base.group_no_one') # first discard all menus with groups the user does not have menus = menus.filtered( lambda menu: not menu.groups_id or menu.groups_id & groups) # take apart menus that have an action action_menus = menus.filtered(lambda m: m.action and m.action.exists()) folder_menus = menus - action_menus visible = self.browse() # process action menus, check whether their action is allowed access = self.env['ir.model.access'] MODEL_GETTER = { 'ir.actions.act_window': lambda action: action.res_model, 'ir.actions.report': lambda action: action.model, 'ir.actions.server': lambda action: action.model_id.model, } for menu in action_menus: get_model = MODEL_GETTER.get(menu.action._name) if not get_model or not get_model(menu.action) or \ access.check(get_model(menu.action), 'read', False): # make menu visible, and its folder ancestors, too visible += menu menu = menu.parent_id while menu and menu in folder_menus and menu not in visible: visible += menu menu = menu.parent_id return set(visible.ids) @api.multi @api.returns('self') def _filter_visible_menus(self): """ Filter `self` to only keep the menu items that should be visible in the menu hierarchy of the current user. Uses a cache for speeding up the computation. """ visible_ids = self._visible_menu_ids(request.debug if request else False) return self.filtered(lambda menu: menu.id in visible_ids) @api.model def search(self, args, offset=0, limit=None, order=None, count=False): menus = super(IrUiMenu, self).search(args, offset=0, limit=None, order=order, count=False) if menus: # menu filtering is done only on main menu tree, not other menu lists if not self._context.get('ir.ui.menu.full_list'): menus = menus._filter_visible_menus() if offset: menus = menus[offset:] if limit: menus = menus[:limit] return len(menus) if count else menus @api.multi def name_get(self): return [(menu.id, menu._get_full_name()) for menu in self] @api.model def create(self, values): self.clear_caches() if 'web_icon' in values: values['web_icon_data'] = self._compute_web_icon_data(values.get('web_icon')) return super(IrUiMenu, self).create(values) @api.multi def write(self, values): self.clear_caches() if 'web_icon' in values: values['web_icon_data'] = self._compute_web_icon_data(values.get('web_icon')) return super(IrUiMenu, self).write(values) def _compute_web_icon_data(self, web_icon): """ Returns the image associated to `web_icon`. `web_icon` can either be: - an image icon [module, path] - a built icon [icon_class, icon_color, background_color] and it only has to call `read_image` if it's an image. """ if web_icon and len(web_icon.split(',')) == 2: return self.read_image(web_icon) @api.multi def unlink(self): # Detach children and promote them to top-level, because it would be unwise to # cascade-delete submenus blindly. We also can't use ondelete=set null because # that is not supported when _parent_store is used (would silently corrupt it). # TODO: ideally we should move them under a generic "Orphans" menu somewhere? extra = {'ir.ui.menu.full_list': True} direct_children = self.with_context(**extra).search([('parent_id', 'in', self.ids)]) direct_children.write({'parent_id': False}) self.clear_caches() return super(IrUiMenu, self).unlink() @api.multi def copy(self, default=None): record = super(IrUiMenu, self).copy(default=default) match = NUMBER_PARENS.search(record.name) if match: next_num = int(match.group(1)) + 1 record.name = NUMBER_PARENS.sub('(%d)' % next_num, record.name) else: record.name = record.name + '(1)' return record @api.model @api.returns('self') def get_user_roots(self): """ Return all root menu ids visible for the user. :return: the root menu ids :rtype: list(int) """ return self.search([('parent_id', '=', False)]) @api.model @tools.ormcache_context('self._uid', keys=('lang',)) def load_menus_root(self): fields = ['name', 'sequence', 'parent_id', 'action', 'web_icon_data'] menu_roots = self.get_user_roots() menu_roots_data = menu_roots.read(fields) if menu_roots else [] menu_root = { 'id': False, 'name': 'root', 'parent_id': [-1, ''], 'children': menu_roots_data, 'all_menu_ids': menu_roots.ids, } menu_roots._set_menuitems_xmlids(menu_root) return menu_root @api.model @tools.ormcache_context('self._uid', 'debug', keys=('lang',)) def load_menus(self, debug): """ Loads all menu items (all applications and their sub-menus). :return: the menu root :rtype: dict('children': menu_nodes) """ fields = ['name', 'sequence', 'parent_id', 'action', 'web_icon', 'web_icon_data'] menu_roots = self.get_user_roots() menu_roots_data = menu_roots.read(fields) if menu_roots else [] menu_root = { 'id': False, 'name': 'root', 'parent_id': [-1, ''], 'children': menu_roots_data, 'all_menu_ids': menu_roots.ids, } if not menu_roots_data: return menu_root # menus are loaded fully unlike a regular tree view, cause there are a # limited number of items (752 when all 6.1 addons are installed) menus = self.search([('id', 'child_of', menu_roots.ids)]) menu_items = menus.read(fields) # add roots at the end of the sequence, so that they will overwrite # equivalent menu items from full menu read when put into id:item # mapping, resulting in children being correctly set on the roots. menu_items.extend(menu_roots_data) menu_root['all_menu_ids'] = menus.ids # includes menu_roots! # make a tree using parent_id menu_items_map = {menu_item["id"]: menu_item for menu_item in menu_items} for menu_item in menu_items: parent = menu_item['parent_id'] and menu_item['parent_id'][0] if parent in menu_items_map: menu_items_map[parent].setdefault( 'children', []).append(menu_item) # sort by sequence a tree using parent_id for menu_item in menu_items: menu_item.setdefault('children', []).sort(key=operator.itemgetter('sequence')) (menu_roots + menus)._set_menuitems_xmlids(menu_root) return menu_root def _set_menuitems_xmlids(self, menu_root): menuitems = self.env['ir.model.data'].sudo().search([ ('res_id', 'in', self.ids), ('model', '=', 'ir.ui.menu') ]) xmlids = { menu.res_id: menu.complete_name for menu in menuitems } def _set_xmlids(tree, xmlids): tree['xmlid'] = xmlids.get(tree['id'], '') if 'children' in tree: for child in tree['children']: _set_xmlids(child, xmlids) _set_xmlids(menu_root, xmlids)
class RepairLine(models.Model): _name = 'mrp.repair.line' _description = 'Repair Line' name = fields.Char('Description', required=True) repair_id = fields.Many2one( 'mrp.repair', 'Repair Order Reference', index=True, ondelete='cascade') type = fields.Selection([ ('add', 'Add'), ('remove', 'Remove')], 'Type', required=True) product_id = fields.Many2one('product.product', 'Product', required=True) invoiced = fields.Boolean('Invoiced', copy=False, readonly=True) price_unit = fields.Float('Unit Price', required=True, digits=dp.get_precision('Product Price')) price_subtotal = fields.Float('Subtotal', compute='_compute_price_subtotal', digits=0) tax_id = fields.Many2many( 'account.tax', 'repair_operation_line_tax', 'repair_operation_line_id', 'tax_id', 'Taxes') product_uom_qty = fields.Float( 'Quantity', default=1.0, digits=dp.get_precision('Product Unit of Measure'), required=True) product_uom = fields.Many2one( 'product.uom', 'Product Unit of Measure', required=True) invoice_line_id = fields.Many2one( 'account.invoice.line', 'Invoice Line', copy=False, readonly=True) location_id = fields.Many2one( 'stock.location', 'Source Location', index=True, required=True) location_dest_id = fields.Many2one( 'stock.location', 'Dest. Location', index=True, required=True) move_id = fields.Many2one( 'stock.move', 'Inventory Move', copy=False, readonly=True) lot_id = fields.Many2one('stock.production.lot', 'Lot/Serial') state = fields.Selection([ ('draft', 'Draft'), ('confirmed', 'Confirmed'), ('done', 'Done'), ('cancel', 'Cancelled')], 'Status', default='draft', copy=False, readonly=True, required=True, help='The status of a repair line is set automatically to the one of the linked repair order.') @api.constrains('lot_id', 'product_id') def constrain_lot_id(self): for line in self.filtered(lambda x: x.product_id.tracking != 'none' and not x.lot_id): raise ValidationError(_("Serial number is required for operation line with product '%s'") % (line.product_id.name)) @api.one @api.depends('price_unit', 'repair_id', 'product_uom_qty', 'product_id', 'repair_id.invoice_method') def _compute_price_subtotal(self): taxes = self.tax_id.compute_all(self.price_unit, self.repair_id.pricelist_id.currency_id, self.product_uom_qty, self.product_id, self.repair_id.partner_id) self.price_subtotal = taxes['total_excluded'] @api.onchange('type', 'repair_id') def onchange_operation_type(self): """ On change of operation type it sets source location, destination location and to invoice field. @param product: Changed operation type. @param guarantee_limit: Guarantee limit of current record. @return: Dictionary of values. """ if not self.type: self.location_id = False self.location_dest_id = False elif self.type == 'add': self.onchange_product_id() args = self.repair_id.company_id and [('company_id', '=', self.repair_id.company_id.id)] or [] warehouse = self.env['stock.warehouse'].search(args, limit=1) self.location_id = warehouse.lot_stock_id self.location_dest_id = self.env['stock.location'].search([('usage', '=', 'production')], limit=1).id else: self.price_unit = 0.0 self.tax_id = False self.location_id = self.env['stock.location'].search([('usage', '=', 'production')], limit=1).id self.location_dest_id = self.env['stock.location'].search([('scrap_location', '=', True)], limit=1).id @api.onchange('repair_id', 'product_id', 'product_uom_qty') def onchange_product_id(self): """ On change of product it sets product quantity, tax account, name, uom of product, unit price and price subtotal. """ partner = self.repair_id.partner_id pricelist = self.repair_id.pricelist_id if not self.product_id or not self.product_uom_qty: return if self.product_id: if partner: self.name = self.product_id.with_context(lang=partner.lang).display_name else: self.name = self.product_id.display_name self.product_uom = self.product_id.uom_id.id if self.type != 'remove': if partner and self.product_id: self.tax_id = partner.property_account_position_id.map_tax(self.product_id.taxes_id, self.product_id, partner).ids warning = False if not pricelist: warning = { 'title': _('No Pricelist!'), 'message': _('You have to select a pricelist in the Repair form !\n Please set one before choosing a product.')} else: price = pricelist.get_product_price(self.product_id, self.product_uom_qty, partner) if price is False: warning = { 'title': _('No valid pricelist line found !'), 'message': _("Couldn't find a pricelist line matching this product and quantity.\nYou have to change either the product, the quantity or the pricelist.")} else: self.price_unit = price if warning: return {'warning': warning}
class Repair(models.Model): _name = 'mrp.repair' _description = 'Repair Order' _inherit = ['mail.thread', 'mail.activity.mixin'] _order = 'create_date desc' @api.model def _default_stock_location(self): warehouse = self.env['stock.warehouse'].search([], limit=1) if warehouse: return warehouse.lot_stock_id.id return False name = fields.Char( 'Repair Reference', default=lambda self: self.env['ir.sequence'].next_by_code('mrp.repair'), copy=False, required=True, states={'confirmed': [('readonly', True)]}) product_id = fields.Many2one( 'product.product', string='Product to Repair', readonly=True, required=True, states={'draft': [('readonly', False)]}) product_qty = fields.Float( 'Product Quantity', default=1.0, digits=dp.get_precision('Product Unit of Measure'), readonly=True, required=True, states={'draft': [('readonly', False)]}) product_uom = fields.Many2one( 'product.uom', 'Product Unit of Measure', readonly=True, required=True, states={'draft': [('readonly', False)]}) partner_id = fields.Many2one( 'res.partner', 'Customer', index=True, states={'confirmed': [('readonly', True)]}, help='Choose partner for whom the order will be invoiced and delivered.') address_id = fields.Many2one( 'res.partner', 'Delivery Address', domain="[('parent_id','=',partner_id)]", states={'confirmed': [('readonly', True)]}) default_address_id = fields.Many2one('res.partner', compute='_compute_default_address_id') state = fields.Selection([ ('draft', 'Quotation'), ('cancel', 'Cancelled'), ('confirmed', 'Confirmed'), ('under_repair', 'Under Repair'), ('ready', 'Ready to Repair'), ('2binvoiced', 'To be Invoiced'), ('invoice_except', 'Invoice Exception'), ('done', 'Repaired')], string='Status', copy=False, default='draft', readonly=True, track_visibility='onchange', help="* The \'Draft\' status is used when a user is encoding a new and unconfirmed repair order.\n" "* The \'Confirmed\' status is used when a user confirms the repair order.\n" "* The \'Ready to Repair\' status is used to start to repairing, user can start repairing only after repair order is confirmed.\n" "* The \'To be Invoiced\' status is used to generate the invoice before or after repairing done.\n" "* The \'Done\' status is set when repairing is completed.\n" "* The \'Cancelled\' status is used when user cancel repair order.") location_id = fields.Many2one( 'stock.location', 'Current Location', default=_default_stock_location, index=True, readonly=True, required=True, states={'draft': [('readonly', False)], 'confirmed': [('readonly', True)]}) location_dest_id = fields.Many2one( 'stock.location', 'Delivery Location', readonly=True, required=True, states={'draft': [('readonly', False)], 'confirmed': [('readonly', True)]}) lot_id = fields.Many2one( 'stock.production.lot', 'Lot/Serial', domain="[('product_id','=', product_id)]", help="Products repaired are all belonging to this lot", oldname="prodlot_id") guarantee_limit = fields.Date('Warranty Expiration', states={'confirmed': [('readonly', True)]}) operations = fields.One2many( 'mrp.repair.line', 'repair_id', 'Parts', copy=True, readonly=True, states={'draft': [('readonly', False)]}) pricelist_id = fields.Many2one( 'product.pricelist', 'Pricelist', default=lambda self: self.env['product.pricelist'].search([], limit=1).id, help='Pricelist of the selected partner.') partner_invoice_id = fields.Many2one('res.partner', 'Invoicing Address') invoice_method = fields.Selection([ ("none", "No Invoice"), ("b4repair", "Before Repair"), ("after_repair", "After Repair")], string="Invoice Method", default='none', index=True, readonly=True, required=True, states={'draft': [('readonly', False)]}, help='Selecting \'Before Repair\' or \'After Repair\' will allow you to generate invoice before or after the repair is done respectively. \'No invoice\' means you don\'t want to generate invoice for this repair order.') invoice_id = fields.Many2one( 'account.invoice', 'Invoice', copy=False, readonly=True, track_visibility="onchange") move_id = fields.Many2one( 'stock.move', 'Move', copy=False, readonly=True, track_visibility="onchange", help="Move created by the repair order") fees_lines = fields.One2many( 'mrp.repair.fee', 'repair_id', 'Operations', copy=True, readonly=True, states={'draft': [('readonly', False)]}) internal_notes = fields.Text('Internal Notes') quotation_notes = fields.Text('Quotation Notes') company_id = fields.Many2one( 'res.company', 'Company', default=lambda self: self.env['res.company']._company_default_get('mrp.repair')) invoiced = fields.Boolean('Invoiced', copy=False, readonly=True) repaired = fields.Boolean('Repaired', copy=False, readonly=True) amount_untaxed = fields.Float('Untaxed Amount', compute='_amount_untaxed', store=True) amount_tax = fields.Float('Taxes', compute='_amount_tax', store=True) amount_total = fields.Float('Total', compute='_amount_total', store=True) tracking = fields.Selection('Product Tracking', related="product_id.tracking") @api.one @api.depends('partner_id') def _compute_default_address_id(self): if self.partner_id: self.default_address_id = self.partner_id.address_get(['contact'])['contact'] @api.one @api.depends('operations.price_subtotal', 'invoice_method', 'fees_lines.price_subtotal', 'pricelist_id.currency_id') def _amount_untaxed(self): total = sum(operation.price_subtotal for operation in self.operations) total += sum(fee.price_subtotal for fee in self.fees_lines) self.amount_untaxed = self.pricelist_id.currency_id.round(total) @api.one @api.depends('operations.price_unit', 'operations.product_uom_qty', 'operations.product_id', 'fees_lines.price_unit', 'fees_lines.product_uom_qty', 'fees_lines.product_id', 'pricelist_id.currency_id', 'partner_id') def _amount_tax(self): val = 0.0 for operation in self.operations: if operation.tax_id: tax_calculate = operation.tax_id.compute_all(operation.price_unit, self.pricelist_id.currency_id, operation.product_uom_qty, operation.product_id, self.partner_id) for c in tax_calculate['taxes']: val += c['amount'] for fee in self.fees_lines: if fee.tax_id: tax_calculate = fee.tax_id.compute_all(fee.price_unit, self.pricelist_id.currency_id, fee.product_uom_qty, fee.product_id, self.partner_id) for c in tax_calculate['taxes']: val += c['amount'] self.amount_tax = val @api.one @api.depends('amount_untaxed', 'amount_tax') def _amount_total(self): self.amount_total = self.pricelist_id.currency_id.round(self.amount_untaxed + self.amount_tax) _sql_constraints = [ ('name', 'unique (name)', 'The name of the Repair Order must be unique!'), ] @api.onchange('product_id') def onchange_product_id(self): self.guarantee_limit = False self.lot_id = False if self.product_id: self.product_uom = self.product_id.uom_id.id @api.onchange('product_uom') def onchange_product_uom(self): res = {} if not self.product_id or not self.product_uom: return res if self.product_uom.category_id != self.product_id.uom_id.category_id: res['warning'] = {'title': _('Warning'), 'message': _('The Product Unit of Measure you chose has a different category than in the product form.')} self.product_uom = self.product_id.uom_id.id return res @api.onchange('location_id') def onchange_location_id(self): self.location_dest_id = self.location_id.id @api.onchange('partner_id') def onchange_partner_id(self): if not self.partner_id: self.address_id = False self.partner_invoice_id = False self.pricelist_id = self.env['product.pricelist'].search([], limit=1).id else: addresses = self.partner_id.address_get(['delivery', 'invoice', 'contact']) self.address_id = addresses['delivery'] or addresses['contact'] self.partner_invoice_id = addresses['invoice'] self.pricelist_id = self.partner_id.property_product_pricelist.id @api.multi def button_dummy(self): # TDE FIXME: this button is very interesting return True @api.multi def action_repair_cancel_draft(self): if self.filtered(lambda repair: repair.state != 'cancel'): raise UserError(_("Repair must be canceled in order to reset it to draft.")) self.mapped('operations').write({'state': 'draft'}) return self.write({'state': 'draft'}) def action_validate(self): self.ensure_one() precision = self.env['decimal.precision'].precision_get('Product Unit of Measure') available_qty = self.env['stock.quant']._get_available_quantity(self.product_id, self.location_id, self.lot_id, strict=True) if float_compare(available_qty, self.product_qty, precision_digits=precision) >= 0: return self.action_repair_confirm() else: return { 'name': _('Insufficient Quantity'), 'view_type': 'form', 'view_mode': 'form', 'res_model': 'stock.warn.insufficient.qty.repair', 'view_id': self.env.ref('mrp_repair.stock_warn_insufficient_qty_repair_form_view').id, 'type': 'ir.actions.act_window', 'context': { 'default_product_id': self.product_id.id, 'default_location_id': self.location_id.id, 'default_repair_id': self.id }, 'target': 'new' } @api.multi def action_repair_confirm(self): """ Repair order state is set to 'To be invoiced' when invoice method is 'Before repair' else state becomes 'Confirmed'. @param *arg: Arguments @return: True """ if self.filtered(lambda repair: repair.state != 'draft'): raise UserError(_("Can only confirm draft repairs.")) before_repair = self.filtered(lambda repair: repair.invoice_method == 'b4repair') before_repair.write({'state': '2binvoiced'}) to_confirm = self - before_repair to_confirm_operations = to_confirm.mapped('operations') to_confirm_operations.write({'state': 'confirmed'}) to_confirm.write({'state': 'confirmed'}) return True @api.multi def action_repair_cancel(self): if self.filtered(lambda repair: repair.state == 'done'): raise UserError(_("Cannot cancel completed repairs.")) if any(repair.invoiced for repair in self): raise UserError(_('Repair order is already invoiced.')) self.mapped('operations').write({'state': 'cancel'}) return self.write({'state': 'cancel'}) @api.multi def action_send_mail(self): self.ensure_one() template_id = self.env.ref('mrp_repair.mail_template_mrp_repair_quotation').id ctx = { 'default_model': 'mrp.repair', 'default_res_id': self.id, 'default_use_template': bool(template_id), 'default_template_id': template_id, 'default_composition_mode': 'comment' } return { 'type': 'ir.actions.act_window', 'view_type': 'form', 'view_mode': 'form', 'res_model': 'mail.compose.message', 'target': 'new', 'context': ctx, } @api.multi def print_repair_order(self): return self.env.ref('mrp_repair.action_report_mrp_repair_order').report_action(self) def action_repair_invoice_create(self): for repair in self: repair.action_invoice_create() if repair.invoice_method == 'b4repair': repair.action_repair_ready() elif repair.invoice_method == 'after_repair': repair.write({'state': 'done'}) return True @api.multi def action_invoice_create(self, group=False): """ Creates invoice(s) for repair order. @param group: It is set to true when group invoice is to be generated. @return: Invoice Ids. """ res = dict.fromkeys(self.ids, False) invoices_group = {} InvoiceLine = self.env['account.invoice.line'] Invoice = self.env['account.invoice'] for repair in self.filtered(lambda repair: repair.state not in ('draft', 'cancel') and not repair.invoice_id): if not repair.partner_id.id and not repair.partner_invoice_id.id: raise UserError(_('You have to select a Partner Invoice Address in the repair form!')) comment = repair.quotation_notes if repair.invoice_method != 'none': if group and repair.partner_invoice_id.id in invoices_group: invoice = invoices_group[repair.partner_invoice_id.id] invoice.write({ 'name': invoice.name + ', ' + repair.name, 'origin': invoice.origin + ', ' + repair.name, 'comment': (comment and (invoice.comment and invoice.comment + "\n" + comment or comment)) or (invoice.comment and invoice.comment or ''), }) else: if not repair.partner_id.property_account_receivable_id: raise UserError(_('No account defined for partner "%s".') % repair.partner_id.name) invoice = Invoice.create({ 'name': repair.name, 'origin': repair.name, 'type': 'out_invoice', 'account_id': repair.partner_id.property_account_receivable_id.id, 'partner_id': repair.partner_invoice_id.id or repair.partner_id.id, 'currency_id': repair.pricelist_id.currency_id.id, 'comment': repair.quotation_notes, 'fiscal_position_id': repair.partner_id.property_account_position_id.id }) invoices_group[repair.partner_invoice_id.id] = invoice repair.write({'invoiced': True, 'invoice_id': invoice.id}) for operation in repair.operations: if operation.type == 'add': if group: name = repair.name + '-' + operation.name else: name = operation.name if operation.product_id.property_account_income_id: account_id = operation.product_id.property_account_income_id.id elif operation.product_id.categ_id.property_account_income_categ_id: account_id = operation.product_id.categ_id.property_account_income_categ_id.id else: raise UserError(_('No account defined for product "%s".') % operation.product_id.name) invoice_line = InvoiceLine.create({ 'invoice_id': invoice.id, 'name': name, 'origin': repair.name, 'account_id': account_id, 'quantity': operation.product_uom_qty, 'invoice_line_tax_ids': [(6, 0, [x.id for x in operation.tax_id])], 'uom_id': operation.product_uom.id, 'price_unit': operation.price_unit, 'price_subtotal': operation.product_uom_qty * operation.price_unit, 'product_id': operation.product_id and operation.product_id.id or False }) operation.write({'invoiced': True, 'invoice_line_id': invoice_line.id}) for fee in repair.fees_lines: if group: name = repair.name + '-' + fee.name else: name = fee.name if not fee.product_id: raise UserError(_('No product defined on Fees!')) if fee.product_id.property_account_income_id: account_id = fee.product_id.property_account_income_id.id elif fee.product_id.categ_id.property_account_income_categ_id: account_id = fee.product_id.categ_id.property_account_income_categ_id.id else: raise UserError(_('No account defined for product "%s".') % fee.product_id.name) invoice_line = InvoiceLine.create({ 'invoice_id': invoice.id, 'name': name, 'origin': repair.name, 'account_id': account_id, 'quantity': fee.product_uom_qty, 'invoice_line_tax_ids': [(6, 0, [x.id for x in fee.tax_id])], 'uom_id': fee.product_uom.id, 'product_id': fee.product_id and fee.product_id.id or False, 'price_unit': fee.price_unit, 'price_subtotal': fee.product_uom_qty * fee.price_unit }) fee.write({'invoiced': True, 'invoice_line_id': invoice_line.id}) invoice.compute_taxes() res[repair.id] = invoice.id return res @api.multi def action_created_invoice(self): self.ensure_one() return { 'name': _('Invoice created'), 'type': 'ir.actions.act_window', 'view_mode': 'form', 'res_model': 'account.invoice', 'view_id': self.env.ref('account.invoice_form').id, 'target': 'current', 'res_id': self.invoice_id.id, } def action_repair_ready(self): self.mapped('operations').write({'state': 'confirmed'}) return self.write({'state': 'ready'}) @api.multi def action_repair_start(self): """ Writes repair order state to 'Under Repair' @return: True """ if self.filtered(lambda repair: repair.state not in ['confirmed', 'ready']): raise UserError(_("Repair must be confirmed before starting reparation.")) self.mapped('operations').write({'state': 'confirmed'}) return self.write({'state': 'under_repair'}) @api.multi def action_repair_end(self): """ Writes repair order state to 'To be invoiced' if invoice method is After repair else state is set to 'Ready'. @return: True """ if self.filtered(lambda repair: repair.state != 'under_repair'): raise UserError(_("Repair must be under repair in order to end reparation.")) for repair in self: repair.write({'repaired': True}) vals = {'state': 'done'} vals['move_id'] = repair.action_repair_done().get(repair.id) if not repair.invoiced and repair.invoice_method == 'after_repair': vals['state'] = '2binvoiced' repair.write(vals) return True @api.multi def action_repair_done(self): """ Creates stock move for operation and stock move for final product of repair order. @return: Move ids of final products """ if self.filtered(lambda repair: not repair.repaired): raise UserError(_("Repair must be repaired in order to make the product moves.")) res = {} Move = self.env['stock.move'] for repair in self: moves = self.env['stock.move'] for operation in repair.operations: move = Move.create({ 'name': repair.name, 'product_id': operation.product_id.id, 'product_uom_qty': operation.product_uom_qty, 'product_uom': operation.product_uom.id, 'partner_id': repair.address_id.id, 'location_id': operation.location_id.id, 'location_dest_id': operation.location_dest_id.id, 'move_line_ids': [(0, 0, {'product_id': operation.product_id.id, 'lot_id': operation.lot_id.id, 'product_uom_qty': 0, # bypass reservation here 'product_uom_id': operation.product_uom.id, 'qty_done': operation.product_uom_qty, 'package_id': False, 'result_package_id': False, 'location_id': operation.location_id.id, #TODO: owner stuff 'location_dest_id': operation.location_dest_id.id,})], 'repair_id': repair.id, 'origin': repair.name, }) moves |= move operation.write({'move_id': move.id, 'state': 'done'}) move = Move.create({ 'name': repair.name, 'product_id': repair.product_id.id, 'product_uom': repair.product_uom.id or repair.product_id.uom_id.id, 'product_uom_qty': repair.product_qty, 'partner_id': repair.address_id.id, 'location_id': repair.location_id.id, 'location_dest_id': repair.location_dest_id.id, 'move_line_ids': [(0, 0, {'product_id': repair.product_id.id, 'lot_id': repair.lot_id.id, 'product_uom_qty': 0, # bypass reservation here 'product_uom_id': repair.product_uom.id or repair.product_id.uom_id.id, 'qty_done': repair.product_qty, 'package_id': False, 'result_package_id': False, 'location_id': repair.location_id.id, #TODO: owner stuff 'location_dest_id': repair.location_dest_id.id,})], 'repair_id': repair.id, 'origin': repair.name, }) consumed_lines = moves.mapped('move_line_ids') produced_lines = move.move_line_ids moves |= move moves._action_done() produced_lines.write({'consume_line_ids': [(6, 0, consumed_lines.ids)]}) res[repair.id] = move.id return res
class HrContract(models.Model): _inherit = 'hr.contract' transport_mode = fields.Selection( [ ('company_car', 'Company car'), ('public_transport', 'Public Transport'), ('others', 'Other'), ], string="Transport", default='company_car', help="Transport mode the employee uses to go to work.") car_atn = fields.Monetary(string='ATN Company Car') public_transport_employee_amount = fields.Monetary( 'Paid by the employee (Monthly)') thirteen_month = fields.Monetary( compute='_compute_holidays_advantages', string='13th Month', help="Yearly gross amount the employee receives as 13th month bonus.") double_holidays = fields.Monetary( compute='_compute_holidays_advantages', string='Holiday Bonus', help="Yearly gross amount the employee receives as holidays bonus.") warrant_value_employee = fields.Monetary( compute='_compute_warrants_cost', string="Warrant value for the employee") # Employer costs fields final_yearly_costs = fields.Monetary( compute='_compute_final_yearly_costs', readonly=False, string='Total Employee Cost', groups="hr.group_hr_manager", track_visibility="onchange", help="Total yearly cost of the employee for the employer.") monthly_yearly_costs = fields.Monetary( compute='_compute_monthly_yearly_costs', string='Monthly Equivalent Cost', readonly=True, help="Total monthly cost of the employee for the employer.") ucm_insurance = fields.Monetary(compute='_compute_ucm_insurance', string="Social Secretary Costs") social_security_contributions = fields.Monetary( compute='_compute_social_security_contributions', string="Social Security Contributions") yearly_cost_before_charges = fields.Monetary( compute='_compute_yearly_cost_before_charges', string="Yearly Costs Before Charges") meal_voucher_paid_by_employer = fields.Monetary( compute='_compute_meal_voucher_paid_by_employer', string="Meal Voucher Paid by Employer") company_car_total_depreciated_cost = fields.Monetary() public_transport_reimbursed_amount = fields.Monetary( string='Reimbursed amount', compute='_compute_public_transport_reimbursed_amount', readonly=False, store=True) others_reimbursed_amount = fields.Monetary(string='Reimbursed amount') transport_employer_cost = fields.Monetary( compute='_compute_transport_employer_cost', string="Employer cost from employee transports") warrants_cost = fields.Monetary(compute='_compute_warrants_cost') # Advantages commission_on_target = fields.Monetary( string="Commission on Target", default=lambda self: self.get_attribute('commission_on_target', 'default_value'), track_visibility="onchange", help= "Monthly gross amount that the employee receives if the target is reached." ) fuel_card = fields.Monetary( string="Fuel Card", default=lambda self: self.get_attribute('fuel_card', 'default_value'), track_visibility="onchange", help="Monthly amount the employee receives on his fuel card.") internet = fields.Monetary( string="Internet", default=lambda self: self.get_attribute('internet', 'default_value'), track_visibility="onchange", help= "The employee's internet subcription will be paid up to this amount.") representation_fees = fields.Monetary( string="Representation Fees", default=lambda self: self.get_attribute('representation_fees', 'default_value'), track_visibility="onchange", help= "Monthly net amount the employee receives to cover his representation fees." ) mobile = fields.Monetary( string="Mobile", default=lambda self: self.get_attribute('mobile', 'default_value'), track_visibility="onchange", help= "The employee's mobile subscription will be paid up to this amount.") mobile_plus = fields.Monetary( string="International Communication", default=lambda self: self.get_attribute('mobile_plus', 'default_value' ), track_visibility="onchange", help= "The employee's mobile subscription for international communication will be paid up to this amount." ) meal_voucher_amount = fields.Monetary( string="Meal Vouchers", default=lambda self: self.get_attribute('meal_voucher_amount', 'default_value'), track_visibility="onchange", help= "Amount the employee receives in the form of meal vouchers per worked day." ) holidays = fields.Float( string='Legal Leaves', default=lambda self: self.get_attribute('holidays', 'default_value'), help="Number of days of paid leaves the employee gets per year.") holidays_editable = fields.Boolean(string="Editable Leaves", default=True) holidays_compensation = fields.Monetary( compute='_compute_holidays_compensation', string="Holidays Compensation") wage_with_holidays = fields.Monetary( compute='_compute_wage_with_holidays', inverse='_inverse_wage_with_holidays', string="Wage update with holidays retenues") additional_net_amount = fields.Monetary( string="Net Supplements", track_visibility="onchange", help="Monthly net amount the employee receives.") retained_net_amount = fields.Monetary( sting="Net Retained", track_visibility="onchange", help="Monthly net amount that is retained on the employee's salary.") eco_checks = fields.Monetary( "Eco Vouchers", default=lambda self: self.get_attribute('eco_checks', 'default_value'), help="Yearly amount the employee receives in the form of eco vouchers." ) @api.depends('holidays', 'wage', 'final_yearly_costs') def _compute_wage_with_holidays(self): for contract in self: if contract.holidays > 20.0: yearly_cost = contract.final_yearly_costs * ( 1.0 - (contract.holidays - 20.0) / 231.0) contract.wage_with_holidays = contract._get_gross_from_employer_costs( yearly_cost) else: contract.wage_with_holidays = contract.wage def _inverse_wage_with_holidays(self): for contract in self: if contract.holidays > 20.0: remaining_for_gross = contract.wage_with_holidays * ( 13.0 + 13.0 * 0.3507 + 0.92) yearly_cost = remaining_for_gross \ + 12.0 * contract.representation_fees \ + 12.0 * contract.fuel_card \ + 12.0 * contract.internet \ + 12.0 * (contract.mobile + contract.mobile_plus) \ + 12.0 * contract.transport_employer_cost \ + contract.warrants_cost \ + 220.0 * contract.meal_voucher_paid_by_employer contract.final_yearly_costs = yearly_cost / ( 1.0 - (contract.holidays - 20.0) / 231.0) contract.wage = contract._get_gross_from_employer_costs( contract.final_yearly_costs) else: contract.wage = contract.wage_with_holidays @api.depends('transport_mode', 'company_car_total_depreciated_cost', 'public_transport_reimbursed_amount', 'others_reimbursed_amount') def _compute_transport_employer_cost(self): for contract in self: if contract.transport_mode == 'company_car': contract.transport_employer_cost = contract.company_car_total_depreciated_cost elif contract.transport_mode == 'public_transport': contract.transport_employer_cost = contract.public_transport_reimbursed_amount elif contract.transport_mode == 'others': contract.transport_employer_cost = contract.others_reimbursed_amount @api.depends('commission_on_target') def _compute_warrants_cost(self): for contract in self: contract.warrants_cost = contract.commission_on_target * 1.326 * 1.05 * 12.0 contract.warrant_value_employee = contract.commission_on_target * 1.326 * ( 1.00 - 0.535) * 12.0 @api.depends('wage', 'fuel_card', 'representation_fees', 'transport_employer_cost', 'internet', 'mobile', 'mobile_plus') def _compute_yearly_cost_before_charges(self): for contract in self: contract.yearly_cost_before_charges = 12.0 * ( contract.wage * (1.0 + 1.0 / 12.0) + contract.fuel_card + contract.representation_fees + contract.internet + contract.mobile + contract.mobile_plus + contract.transport_employer_cost) @api.depends('yearly_cost_before_charges', 'social_security_contributions', 'wage', 'social_security_contributions', 'double_holidays', 'warrants_cost', 'meal_voucher_paid_by_employer') def _compute_final_yearly_costs(self): for contract in self: contract.final_yearly_costs = ( contract.yearly_cost_before_charges + contract.social_security_contributions + contract.double_holidays + contract.warrants_cost + (220.0 * contract.meal_voucher_paid_by_employer)) @api.depends('holidays', 'final_yearly_costs') def _compute_holidays_compensation(self): for contract in self: if contract.holidays < 20: decrease_amount = contract.final_yearly_costs * ( 20.0 - contract.holidays) / 231.0 contract.holidays_compensation = decrease_amount else: contract.holidays_compensation = 0.0 @api.onchange('final_yearly_costs') def _onchange_final_yearly_costs(self): self.wage = self._get_gross_from_employer_costs( self.final_yearly_costs) @api.depends('meal_voucher_amount') def _compute_meal_voucher_paid_by_employer(self): for contract in self: contract.meal_voucher_paid_by_employer = contract.meal_voucher_amount * ( 1 - 0.1463) @api.depends('wage') def _compute_social_security_contributions(self): for contract in self: total_wage = contract.wage * 13.0 contract.social_security_contributions = (total_wage) * 0.3507 @api.depends('wage') def _compute_ucm_insurance(self): for contract in self: contract.ucm_insurance = (contract.wage * 12.0) * 0.05 @api.depends('public_transport_employee_amount') def _compute_public_transport_reimbursed_amount(self): for contract in self: contract.public_transport_reimbursed_amount = contract._get_public_transport_reimbursed_amount( contract.public_transport_employee_amount) def _get_public_transport_reimbursed_amount(self, amount): return amount * 0.68 @api.depends('final_yearly_costs') def _compute_monthly_yearly_costs(self): for contract in self: contract.monthly_yearly_costs = contract.final_yearly_costs / 12.0 @api.depends('wage') def _compute_holidays_advantages(self): for contract in self: contract.double_holidays = contract.wage * 0.92 contract.thirteen_month = contract.wage @api.onchange('transport_mode') def _onchange_transport_mode(self): if self.transport_mode != 'company_car': self.fuel_card = 0 self.company_car_total_depreciated_cost = 0 if self.transport_mode != 'others': self.others_reimbursed_amount = 0 if self.transport_mode != 'public_transports': self.public_transport_reimbursed_amount = 0 @api.onchange('mobile', 'mobile_plus') def _onchange_mobile(self): if self.mobile_plus and not self.mobile: raise ValidationError( _('You should have a mobile subscription to select an international communication amount!' )) def _get_internet_amount(self, has_internet): if has_internet: return self.get_attribute('internet', 'default_value') else: return 0.0 def _get_mobile_amount(self, has_mobile, international_communication): if has_mobile and international_communication: return self.get_attribute('mobile', 'default_value') + self.get_attribute( 'mobile_plus', 'default_value') elif has_mobile: return self.get_attribute('mobile', 'default_value') else: return 0.0 def _get_gross_from_employer_costs(self, yearly_cost): contract = self remaining_for_gross = yearly_cost \ - 12.0 * contract.representation_fees \ - 12.0 * contract.fuel_card \ - 12.0 * contract.internet \ - 12.0 * (contract.mobile + contract.mobile_plus) \ - 12.0 * contract.transport_employer_cost \ - contract.warrants_cost \ - 220.0 * contract.meal_voucher_paid_by_employer gross = remaining_for_gross / (13.0 + 13.0 * 0.3507 + 0.92) return gross
class HrEmployee(models.Model): _inherit = 'hr.employee' spouse_fiscal_status = fields.Selection( [('without income', 'Without Income'), ('with income', 'With Income')], string='Tax status for spouse', groups="hr.group_hr_user") disabled = fields.Boolean( string="Disabled", help="If the employee is declared disabled by law", groups="hr.group_hr_user") disabled_spouse_bool = fields.Boolean( string='Disabled Spouse', help='if recipient spouse is declared disabled by law', groups="hr.group_hr_user") disabled_children_bool = fields.Boolean( string='Disabled Children', help='if recipient children is/are declared disabled by law', groups="hr.group_hr_user") resident_bool = fields.Boolean( string='Nonresident', help='if recipient lives in a foreign country', groups="hr.group_hr_user") disabled_children_number = fields.Integer('Number of disabled children', groups="hr.group_hr_user") dependent_children = fields.Integer( compute='_compute_dependent_children', string='Considered number of dependent children', groups="hr.group_hr_user") other_dependent_people = fields.Boolean( string="Other Dependent People", help="If other people are dependent on the employee", groups="hr.group_hr_user") other_senior_dependent = fields.Integer( '# seniors (>=65)', help= "Number of seniors dependent on the employee, including the disabled ones", groups="hr.group_hr_user") other_disabled_senior_dependent = fields.Integer( '# disabled seniors (>=65)', groups="hr.group_hr_user") other_juniors_dependent = fields.Integer( '# people (<65)', help= "Number of juniors dependent on the employee, including the disabled ones", groups="hr.group_hr_user") other_disabled_juniors_dependent = fields.Integer( '# disabled people (<65)', groups="hr.group_hr_user") dependent_seniors = fields.Integer( compute='_compute_dependent_people', string="Considered number of dependent seniors", groups="hr.group_hr_user") dependent_juniors = fields.Integer( compute='_compute_dependent_people', string="Considered number of dependent juniors", groups="hr.group_hr_user") spouse_net_revenue = fields.Float( string="Spouse Net Revenue", help= "Own professional income, other than pensions, annuities or similar income", groups="hr.group_hr_user") spouse_other_net_revenue = fields.Float( string="Spouse Other Net Revenue", help= 'Own professional income which is exclusively composed of pensions, annuities or similar income', groups="hr.group_hr_user") @api.constrains('spouse_fiscal_status', 'spouse_net_revenue', 'spouse_other_net_revenue') def _check_spouse_revenue(self): for employee in self: if employee.spouse_fiscal_status == 'with income' and not employee.spouse_net_revenue and not employee.spouse_other_net_revenue: raise ValidationError( _("The revenue for the spouse can't be equal to zero is the fiscal status is 'With Income'." )) @api.onchange('spouse_fiscal_status') def _onchange_spouse_fiscal_status(self): self.spouse_net_revenue = 0.0 self.spouse_other_net_revenue = 0.0 @api.onchange('disabled_children_bool') def _onchange_disabled_children_bool(self): self.disabled_children_number = 0 @api.onchange('other_dependent_people') def _onchange_other_dependent_people(self): self.other_senior_dependent = 0.0 self.other_disabled_senior_dependent = 0.0 self.other_juniors_dependent = 0.0 self.other_disabled_juniors_dependent = 0.0 @api.depends('disabled_children_bool', 'disabled_children_number', 'children') def _compute_dependent_children(self): for employee in self: if employee.disabled_children_bool: employee.dependent_children = employee.children + employee.disabled_children_number else: employee.dependent_children = employee.children @api.depends('other_dependent_people', 'other_senior_dependent', 'other_disabled_senior_dependent', 'other_juniors_dependent', 'other_disabled_juniors_dependent') def _compute_dependent_people(self): for employee in self: employee.dependent_seniors = employee.other_senior_dependent + employee.other_disabled_senior_dependent employee.dependent_juniors = employee.other_juniors_dependent + employee.other_disabled_juniors_dependent
class Partner(models.Model): """ Update partner to add a field about notification preferences. Add a generic opt-out field that can be used to restrict usage of automatic email templates. """ _name = "res.partner" _inherit = ['res.partner', 'mail.thread', 'mail.activity.mixin'] _mail_flat_thread = False message_bounce = fields.Integer( 'Bounce', help="Counter of the number of bounced emails for this contact", default=0) opt_out = fields.Boolean( 'Opt-Out', help= "If opt-out is checked, this contact has refused to receive emails for mass mailing and marketing campaign. " "Filter 'Available for Mass Mailing' allows users to filter the partners when performing mass mailing." ) channel_ids = fields.Many2many('mail.channel', 'mail_channel_partner', 'partner_id', 'channel_id', string='Channels', copy=False) @api.multi def message_get_suggested_recipients(self): recipients = super(Partner, self).message_get_suggested_recipients() for partner in self: partner._message_add_suggested_recipient( recipients, partner=partner, reason=_('Partner Profile')) return recipients @api.multi def message_get_default_recipients(self): return dict((res_id, { 'partner_ids': [res_id], 'email_to': False, 'email_cc': False }) for res_id in self.ids) @api.model def _notify_prepare_template_context(self, message): # compute signature signature = "" if message.author_id and message.author_id.user_ids and message.author_id.user_ids[ 0].signature: signature = message.author_id.user_ids[0].signature elif message.author_id: signature = "<p>-- <br/>%s</p>" % message.author_id.name # compute Sent by if message.author_id and message.author_id.user_ids: user = message.author_id.user_ids[0] else: user = self.env.user if user.company_id.website: website_url = 'http://%s' % user.company_id.website if not user.company_id.website.lower( ).startswith(('http:', 'https:')) else user.company_id.website else: website_url = False model_name = False if message.model: model_name = self.env['ir.model']._get(message.model).display_name record_name = message.record_name tracking = [] for tracking_value in self.env['mail.tracking.value'].sudo().search([ ('mail_message_id', '=', message.id) ]): tracking.append((tracking_value.field_desc, tracking_value.get_old_display_value()[0], tracking_value.get_new_display_value()[0])) is_discussion = message.subtype_id.id == self.env[ 'ir.model.data'].xmlid_to_res_id('mail.mt_comment') record = False if message.res_id and message.model in self.env: record = self.env[message.model].browse(message.res_id) company = user.company_id if record and hasattr(record, 'company_id'): company = record.company_id company_name = company.name return { 'signature': signature, 'website_url': website_url, 'company': company, 'company_name': company_name, 'model_name': model_name, 'record': record, 'record_name': record_name, 'tracking': tracking, 'is_discussion': is_discussion, 'subtype': message.subtype_id, } @api.model def _notify_prepare_email_values(self, message): # compute email references references = message.parent_id.message_id if message.parent_id else False # custom values custom_values = dict() if message.res_id and message.model in self.env and hasattr( self.env[message.model], 'message_get_email_values'): custom_values = self.env[message.model].browse( message.res_id).message_get_email_values(message) mail_values = { 'mail_message_id': message.id, 'mail_server_id': message.mail_server_id.id, 'auto_delete': self._context.get('mail_auto_delete', True), 'keep_days': self._context.get('mail_keep_days', -1), 'references': references, } mail_values.update(custom_values) return mail_values @api.model def _notify_send(self, body, subject, recipients, **mail_values): emails = self.env['mail.mail'] recipients_nbr = len(recipients) for email_chunk in split_every(50, recipients.ids): # TDE FIXME: missing message parameter. So we will find mail_message_id # in the mail_values and browse it. It should already be in the # cache so should not impact performances. mail_message_id = mail_values.get('mail_message_id') message = self.env['mail.message'].browse( mail_message_id) if mail_message_id else None if message and message.model and message.res_id and message.model in self.env and hasattr( self.env[message.model], 'message_get_recipient_values'): tig = self.env[message.model].browse(message.res_id) recipient_values = tig.message_get_recipient_values( notif_message=message, recipient_ids=email_chunk) else: recipient_values = self.env[ 'mail.thread'].message_get_recipient_values( notif_message=None, recipient_ids=email_chunk) create_values = { 'body_html': body, 'subject': subject, } create_values.update(mail_values) create_values.update(recipient_values) emails |= self.env['mail.mail'].create(create_values) return emails, recipients_nbr @api.model def _notify_udpate_notifications(self, emails): for email in emails: notifications = self.env['mail.notification'].sudo().search([ ('mail_message_id', '=', email.mail_message_id.id), ('res_partner_id', 'in', email.recipient_ids.ids) ]) notifications.write({ 'is_email': True, 'is_read': True, # handle by email discards Inbox notification 'email_status': 'ready', }) @api.multi def _notify(self, message, force_send=False, send_after_commit=True, user_signature=True): """ Method to send email linked to notified messages. The recipients are the recordset on which this method is called. :param boolean force_send: send notification emails now instead of letting the scheduler handle the email queue :param boolean send_after_commit: send notification emails after the transaction end instead of durign the transaction; this option is used only if force_send is True :param user_signature: add current user signature to notification emails """ if not self.ids: return True # existing custom notification email base_template = None if message.model and self._context.get('custom_layout', False): base_template = self.env.ref(self._context['custom_layout'], raise_if_not_found=False) if not base_template: base_template = self.env.ref( 'mail.mail_template_data_notification_email_default') base_template_ctx = self._notify_prepare_template_context(message) if not user_signature: base_template_ctx['signature'] = False base_mail_values = self._notify_prepare_email_values(message) # classify recipients: actions / no action if message.model and message.res_id and hasattr( self.env[message.model], '_message_notification_recipients'): recipients = self.env[message.model].browse( message.res_id)._message_notification_recipients( message, self) else: recipients = self.env[ 'mail.thread']._message_notification_recipients(message, self) emails = self.env['mail.mail'] recipients_nbr, recipients_max = 0, 50 for email_type, recipient_template_values in recipients.items(): if recipient_template_values['followers']: # generate notification email content template_fol_values = dict( base_template_ctx, **recipient_template_values ) # fixme: set button_unfollow to none template_fol_values['has_button_follow'] = False template_fol = base_template.with_context( **template_fol_values) # generate templates for followers and not followers fol_values = template_fol.generate_email( message.id, fields=['body_html', 'subject']) # send email new_emails, new_recipients_nbr = self._notify_send( fol_values['body'], fol_values['subject'], recipient_template_values['followers'], **base_mail_values) # update notifications self._notify_udpate_notifications(new_emails) emails |= new_emails recipients_nbr += new_recipients_nbr if recipient_template_values['not_followers']: # generate notification email content template_not_values = dict( base_template_ctx, **recipient_template_values ) # fixme: set button_follow to none template_not_values['has_button_unfollow'] = False template_not = base_template.with_context( **template_not_values) # generate templates for followers and not followers not_values = template_not.generate_email( message.id, fields=['body_html', 'subject']) # send email new_emails, new_recipients_nbr = self._notify_send( not_values['body'], not_values['subject'], recipient_template_values['not_followers'], **base_mail_values) # update notifications self._notify_udpate_notifications(new_emails) emails |= new_emails recipients_nbr += new_recipients_nbr # NOTE: # 1. for more than 50 followers, use the queue system # 2. do not send emails immediately if the registry is not loaded, # to prevent sending email during a simple update of the database # using the command-line. test_mode = getattr(threading.currentThread(), 'testing', False) if force_send and recipients_nbr < recipients_max and \ (not self.pool._init or test_mode): email_ids = emails.ids dbname = self.env.cr.dbname _context = self._context def send_notifications(): db_registry = registry(dbname) with api.Environment.manage(), db_registry.cursor() as cr: env = api.Environment(cr, SUPERUSER_ID, _context) env['mail.mail'].browse(email_ids).send() # unless asked specifically, send emails after the transaction to # avoid side effects due to emails being sent while the transaction fails if not test_mode and send_after_commit: self._cr.after('commit', send_notifications) else: emails.send() return True @api.multi def _notify_by_chat(self, message): """ Broadcast the message to all the partner since """ message_values = message.message_format()[0] notifications = [] for partner in self: notifications.append([(self._cr.dbname, 'ir.needaction', partner.id), dict(message_values)]) self.env['bus.bus'].sendmany(notifications) @api.model def get_needaction_count(self): """ compute the number of needaction of the current user """ if self.env.user.partner_id: self.env.cr.execute( """ SELECT count(*) as needaction_count FROM mail_message_res_partner_needaction_rel R WHERE R.res_partner_id = %s AND (R.is_read = false OR R.is_read IS NULL)""", (self.env.user.partner_id.id, )) return self.env.cr.dictfetchall()[0].get('needaction_count') _logger.error('Call to needaction_count without partner_id') return 0 @api.model def get_starred_count(self): """ compute the number of starred of the current user """ if self.env.user.partner_id: self.env.cr.execute( """ SELECT count(*) as starred_count FROM mail_message_res_partner_starred_rel R WHERE R.res_partner_id = %s """, (self.env.user.partner_id.id, )) return self.env.cr.dictfetchall()[0].get('starred_count') _logger.error('Call to starred_count without partner_id') return 0 @api.model def get_static_mention_suggestions(self): """ To be overwritten to return the id, name and email of partners used as static mention suggestions loaded once at webclient initialization and stored client side. """ return [] @api.model def get_mention_suggestions(self, search, limit=8): """ Return 'limit'-first partners' id, name and email such that the name or email matches a 'search' string. Prioritize users, and then extend the research to all partners. """ search_dom = expression.OR([[('name', 'ilike', search)], [('email', 'ilike', search)]]) fields = ['id', 'name', 'email'] # Search users domain = expression.AND([[('user_ids.id', '!=', False)], search_dom]) users = self.search_read(domain, fields, limit=limit) # Search partners if less than 'limit' users found partners = [] if len(users) < limit: partners = self.search_read(search_dom, fields, limit=limit) # Remove duplicates partners = [ p for p in partners if not len([u for u in users if u['id'] == p['id']]) ] return [users, partners]
class ResourceResource(models.Model): _name = "resource.resource" _description = "Resource Detail" @api.model def default_get(self, fields): res = super(ResourceResource, self).default_get(fields) if not res.get('calendar_id') and res.get('company_id'): company = self.env['res.company'].browse(res['company_id']) res['calendar_id'] = company.resource_calendar_id.id return res name = fields.Char(required=True) active = fields.Boolean( 'Active', default=True, track_visibility='onchange', help="If the active field is set to False, it will allow you to hide the resource record without removing it.") company_id = fields.Many2one('res.company', string='Company', default=lambda self: self.env['res.company']._company_default_get()) resource_type = fields.Selection([ ('user', 'Human'), ('material', 'Material')], string='Resource Type', default='user', required=True) user_id = fields.Many2one('res.users', string='User', help='Related user name for the resource to manage its access.') time_efficiency = fields.Float( 'Efficiency Factor', default=100, required=True, help="This field is used to calculate the the expected duration of a work order at this work center. For example, if a work order takes one hour and the efficiency factor is 100%, then the expected duration will be one hour. If the efficiency factor is 200%, however the expected duration will be 30 minutes.") calendar_id = fields.Many2one( "resource.calendar", string='Working Time', default=lambda self: self.env['res.company']._company_default_get().resource_calendar_id, required=True, help="Define the schedule of resource") _sql_constraints = [ ('check_time_efficiency', 'CHECK(time_efficiency>0)', 'Time efficiency must be strictly positive'), ] @api.multi @api.constrains('time_efficiency') def _check_time_efficiency(self): for record in self: if record.time_efficiency == 0: raise ValidationError(_('The efficiency factor cannot be equal to 0.')) @api.model def create(self, values): if values.get('company_id') and not values.get('calendar_id'): values['calendar_id'] = self.env['res.company'].browse(values['company_id']).resource_calendar_id.id return super(ResourceResource, self).create(values) @api.multi def copy(self, default=None): self.ensure_one() if default is None: default = {} if not default.get('name'): default.update(name=_('%s (copy)') % (self.name)) return super(ResourceResource, self).copy(default) @api.onchange('company_id') def _onchange_company_id(self): if self.company_id: self.calendar_id = self.company_id.resource_calendar_id.id
class Invite(models.TransientModel): """ Wizard to invite partners (or channels) and make them followers. """ _name = 'mail.wizard.invite' _description = 'Invite wizard' @api.model def default_get(self, fields): result = super(Invite, self).default_get(fields) user_name = self.env.user.name_get()[0][1] model = result.get('res_model') res_id = result.get('res_id') if self._context.get('mail_invite_follower_channel_only'): result['send_mail'] = False if 'message' in fields and model and res_id: model_name = self.env['ir.model']._get(model).display_name document_name = self.env[model].browse(res_id).name_get()[0][1] message = _( '<div><p>Hello,</p><p>%s invited you to follow %s document: %s.</p></div>' ) % (user_name, model_name, document_name) result['message'] = message elif 'message' in fields: result['message'] = _( '<div><p>Hello,</p><p>%s invited you to follow a new document.</p></div>' ) % user_name return result res_model = fields.Char('Related Document Model', required=True, index=True, help='Model of the followed resource') res_id = fields.Integer('Related Document ID', index=True, help='Id of the followed resource') partner_ids = fields.Many2many( 'res.partner', string='Recipients', help= "List of partners that will be added as follower of the current document." ) channel_ids = fields.Many2many( 'mail.channel', string='Channels', help= 'List of channels that will be added as listeners of the current document.', domain=[('channel_type', '=', 'channel')]) message = fields.Html('Message') send_mail = fields.Boolean( 'Send Email', default=True, help= "If checked, the partners will receive an email warning they have been added in the document's followers." ) @api.multi def add_followers(self): email_from = self.env['mail.message']._get_default_from() for wizard in self: Model = self.env[wizard.res_model] document = Model.browse(wizard.res_id) # filter partner_ids to get the new followers, to avoid sending email to already following partners new_partners = wizard.partner_ids - document.message_partner_ids new_channels = wizard.channel_ids - document.message_channel_ids document.message_subscribe(new_partners.ids, new_channels.ids) model_name = self.env['ir.model']._get( wizard.res_model).display_name # send an email if option checked and if a message exists (do not send void emails) if wizard.send_mail and wizard.message and not wizard.message == '<br>': # when deleting the message, cleditor keeps a <br> message = self.env['mail.message'].create({ 'subject': _('Invitation to follow %s: %s') % (model_name, document.name_get()[0][1]), 'body': wizard.message, 'record_name': document.name_get()[0][1], 'email_from': email_from, 'reply_to': email_from, 'model': wizard.res_model, 'res_id': wizard.res_id, 'no_auto_thread': True, }) new_partners.with_context(auto_delete=True)._notify( message, force_send=True, send_after_commit=False, user_signature=True) message.unlink() return {'type': 'ir.actions.act_window_close'}
class DeliveryCarrier(models.Model): _name = 'delivery.carrier' _description = "Carrier" _order = 'sequence, id' ''' A Shipping Provider In order to add your own external provider, follow these steps: 1. Create your model MyProvider that _inherit 'delivery.carrier' 2. Extend the selection of the field "delivery_type" with a pair ('<my_provider>', 'My Provider') 3. Add your methods: <my_provider>_rate_shipment <my_provider>_send_shipping <my_provider>_get_tracking_link <my_provider>_cancel_shipment (they are documented hereunder) ''' # -------------------------------- # # Internals for shipping providers # # -------------------------------- # name = fields.Char(required=True) active = fields.Boolean(default=True) sequence = fields.Integer(help="Determine the display order", default=10) # This field will be overwritten by internal shipping providers by adding their own type (ex: 'fedex') delivery_type = fields.Selection([('fixed', 'Fixed Price')], string='Provider', default='fixed', required=True) integration_level = fields.Selection([('rate', 'Get Rate'), ('rate_and_ship', 'Get Rate and Create Shipment')], string="Integration Level", default='rate_and_ship', help="Action while validating Delivery Orders") prod_environment = fields.Boolean("Environment", help="Set to True if your credentials are certified for production.") debug_logging = fields.Boolean('Debug logging', help="Log requests in order to ease debugging") company_id = fields.Many2one('res.company', string='Company', related='product_id.company_id', store=True) product_id = fields.Many2one('product.product', string='Delivery Product', required=True, ondelete='restrict') country_ids = fields.Many2many('res.country', 'delivery_carrier_country_rel', 'carrier_id', 'country_id', 'Countries') state_ids = fields.Many2many('res.country.state', 'delivery_carrier_state_rel', 'carrier_id', 'state_id', 'States') zip_from = fields.Char('Zip From') zip_to = fields.Char('Zip To') margin = fields.Integer(help='This percentage will be added to the shipping price.') free_over = fields.Boolean('Free if order amount is above', help="If the order total amount (shipping excluded) is above or equal to this value, the customer benefits from a free shipping", default=False, oldname='free_if_more_than') amount = fields.Float(string='Amount', help="Amount of the order to benefit from a free shipping, expressed in the company currency") _sql_constraints = [ ('margin_not_under_100_percent', 'CHECK (margin >= -100)', 'Margin cannot be lower than -100%'), ] def toggle_prod_environment(self): for c in self: c.prod_environment = not c.prod_environment def toggle_debug(self): for c in self: c.debug_logging = not c.debug_logging @api.multi def install_more_provider(self): return { 'name': 'New Providers', 'view_mode': 'kanban', 'res_model': 'ir.module.module', 'domain': [['name', 'ilike', 'delivery_']], 'type': 'ir.actions.act_window', 'help': _('''<p class="oe_view_nocontent"> Buy actpy Enterprise now to get more providers. </p>'''), } def available_carriers(self, partner): return self.filtered(lambda c: c._match_address(partner)) def _match_address(self, partner): self.ensure_one() if self.country_ids and partner.country_id not in self.country_ids: return False if self.state_ids and partner.state_id not in self.state_ids: return False if self.zip_from and (partner.zip or '').upper() < self.zip_from.upper(): return False if self.zip_to and (partner.zip or '').upper() > self.zip_to.upper(): return False return True @api.onchange('state_ids') def onchange_states(self): self.country_ids = [(6, 0, self.country_ids.ids + self.state_ids.mapped('country_id.id'))] @api.onchange('country_ids') def onchange_countries(self): self.state_ids = [(6, 0, self.state_ids.filtered(lambda state: state.id in self.country_ids.mapped('state_ids').ids).ids)] # -------------------------- # # API for external providers # # -------------------------- # def rate_shipment(self, order): ''' Compute the price of the order shipment :param order: record of sale.order :return dict: {'success': boolean, 'price': a float, 'error_message': a string containing an error message, 'warning_message': a string containing a warning message} # TODO maybe the currency code? ''' self.ensure_one() if hasattr(self, '%s_rate_shipment' % self.delivery_type): res = getattr(self, '%s_rate_shipment' % self.delivery_type)(order) # apply margin on computed price res['price'] = res['price'] * (1.0 + (float(self.margin) / 100.0)) # free when order is large enough if res['success'] and self.free_over and order._compute_amount_total_without_delivery() >= self.amount: res['warning_message'] = _('Info:\nThe shipping is free because the order amount exceeds %.2f.\n(The actual shipping cost is: %.2f)') % (self.amount, res['price']) res['price'] = 0.0 return res def send_shipping(self, pickings): ''' Send the package to the service provider :param pickings: A recordset of pickings :return list: A list of dictionaries (one per picking) containing of the form:: { 'exact_price': price, 'tracking_number': number } # TODO missing labels per package # TODO missing currency # TODO missing success, error, warnings ''' self.ensure_one() if hasattr(self, '%s_send_shipping' % self.delivery_type): return getattr(self, '%s_send_shipping' % self.delivery_type)(pickings) def get_tracking_link(self, picking): ''' Ask the tracking link to the service provider :param picking: record of stock.picking :return str: an URL containing the tracking link or False ''' self.ensure_one() if hasattr(self, '%s_get_tracking_link' % self.delivery_type): return getattr(self, '%s_get_tracking_link' % self.delivery_type)(picking) def cancel_shipment(self, pickings): ''' Cancel a shipment :param pickings: A recordset of pickings ''' self.ensure_one() if hasattr(self, '%s_cancel_shipment' % self.delivery_type): return getattr(self, '%s_cancel_shipment' % self.delivery_type)(pickings) def log_xml(self, xml_string, func): self.ensure_one() if self.debug_logging: db_name = self._cr.dbname # Use a new cursor to avoid rollback that could be caused by an upper method try: db_registry = registry(db_name) with db_registry.cursor() as cr: env = api.Environment(cr, SUPERUSER_ID, {}) IrLogging = env['ir.logging'] IrLogging.sudo().create({'name': 'delivery.carrier', 'type': 'server', 'dbname': db_name, 'level': 'DEBUG', 'message': xml_string, 'path': self.delivery_type, 'func': func, 'line': 1}) except psycopg2.Error: pass # ------------------------------------------------ # # Fixed price shipping, aka a very simple provider # # ------------------------------------------------ # fixed_price = fields.Float(compute='_compute_fixed_price', inverse='_set_product_fixed_price', store=True, string='Fixed Price') @api.depends('product_id.list_price', 'product_id.product_tmpl_id.list_price') def _compute_fixed_price(self): for carrier in self: carrier.fixed_price = carrier.product_id.list_price def _set_product_fixed_price(self): for carrier in self: carrier.product_id.list_price = carrier.fixed_price def fixed_rate_shipment(self, order): price = self.fixed_price if self.company_id.currency_id.id != order.currency_id.id: price = self.env['res.currency']._compute(self.company_id.currency_id, order.currency_id, price) return {'success': True, 'price': price, 'error_message': False, 'warning_message': False} def fixed_send_shipping(self, pickings): res = [] for p in pickings: res = res + [{'exact_price': p.carrier_id.fixed_price, 'tracking_number': False}] return res def fixed_get_tracking_link(self, picking): return False def fixed_cancel_shipment(self, pickings): raise NotImplementedError()
class StockMove(models.Model): _inherit = "stock.move" to_refund = fields.Boolean( string="To Refund (update SO/PO)", copy=False, help= 'Trigger a decrease of the delivered/received quantity in the associated Sale Order/Purchase Order' ) value = fields.Float(copy=False) remaining_qty = fields.Float(copy=False) remaining_value = fields.Float(copy=False) account_move_ids = fields.One2many('account.move', 'stock_move_id') @api.multi def action_get_account_moves(self): self.ensure_one() action_ref = self.env.ref('account.action_move_journal_line') if not action_ref: return False action_data = action_ref.read()[0] action_data['domain'] = [('id', 'in', self.account_move_ids.ids)] return action_data def _get_price_unit(self): """ Returns the unit price to store on the quant """ return self.price_unit or self.product_id.standard_price @api.model def _get_in_base_domain(self, company_id=False): domain = [('state', '=', 'done'), ('location_id.company_id', '=', False), ('location_dest_id.company_id', '=', company_id or self.env.user.company_id.id)] return domain @api.model def _get_all_base_domain(self, company_id=False): domain = [('state', '=', 'done'), '|', '&', ('location_id.company_id', '=', False), ('location_dest_id.company_id', '=', company_id or self.env.user.company_id.id), '&', ('location_id.company_id', '=', company_id or self.env.user.company_id.id), ('location_dest_id.company_id', '=', False)] return domain def _get_in_domain(self): return [('product_id', '=', self.product_id.id) ] + self._get_in_base_domain(company_id=self.company_id.id) def _get_all_domain(self): return [('product_id', '=', self.product_id.id) ] + self._get_all_base_domain(company_id=self.company_id.id) def _is_in(self): """ Check if the move should be considered as entering the company so that the cost method will be able to apply the correct logic. :return: True if the move is entering the company else False """ for move_line in self.move_line_ids.filtered( lambda ml: not ml.owner_id): if not move_line.location_id._should_be_valued( ) and move_line.location_dest_id._should_be_valued(): return True return False def _is_out(self): """ Check if the move should be considered as leaving the company so that the cost method will be able to apply the correct logic. :return: True if the move is leaving the company else False """ for move_line in self.move_line_ids.filtered( lambda ml: not ml.owner_id): if move_line.location_id._should_be_valued( ) and not move_line.location_dest_id._should_be_valued(): return True return False def _is_dropshipped(self): """ Check if the move should be considered as a dropshipping move so that the cost method will be able to apply the correct logic. :return: True if the move is a dropshipping one else False """ return self.location_id.usage == 'supplier' and self.location_dest_id.usage == 'customer' @api.model def _run_fifo(self, move, quantity=None): move.ensure_one() # Find back incoming stock moves (called candidates here) to value this move. valued_move_lines = move.move_line_ids.filtered( lambda ml: ml.location_id._should_be_valued() and not ml. location_dest_id._should_be_valued() and not ml.owner_id) valued_quantity = 0 for valued_move_line in valued_move_lines: valued_quantity += valued_move_line.product_uom_id._compute_quantity( valued_move_line.qty_done, move.product_id.uom_id) qty_to_take_on_candidates = quantity or valued_quantity candidates = move.product_id._get_fifo_candidates_in_move() new_standard_price = 0 tmp_value = 0 # to accumulate the value taken on the candidates for candidate in candidates: new_standard_price = candidate.price_unit if candidate.remaining_qty <= qty_to_take_on_candidates: qty_taken_on_candidate = candidate.remaining_qty else: qty_taken_on_candidate = qty_to_take_on_candidates # As applying a landed cost do not update the unit price, naivelly doing # something like qty_taken_on_candidate * candidate.price_unit won't make # the additional value brought by the landed cost go away. candidate_price_unit = candidate.remaining_value / candidate.remaining_qty value_taken_on_candidate = qty_taken_on_candidate * candidate_price_unit candidate_vals = { 'remaining_qty': candidate.remaining_qty - qty_taken_on_candidate, 'remaining_value': candidate.remaining_value - value_taken_on_candidate, } candidate.write(candidate_vals) qty_to_take_on_candidates -= qty_taken_on_candidate tmp_value += value_taken_on_candidate if qty_to_take_on_candidates == 0: break # Update the standard price with the price of the last used candidate, if any. if new_standard_price and move.product_id.cost_method == 'fifo': move.product_id.standard_price = new_standard_price # If there's still quantity to value but we're out of candidates, we fall in the # negative stock use case. We chose to value the out move at the price of the # last out and a correction entry will be made once `_fifo_vacuum` is called. if qty_to_take_on_candidates == 0: move.write({ 'value': -tmp_value if not quantity else move.value or -tmp_value, # outgoing move are valued negatively 'price_unit': -tmp_value / move.product_qty, }) elif qty_to_take_on_candidates > 0: last_fifo_price = new_standard_price or move.product_id.standard_price negative_stock_value = last_fifo_price * -qty_to_take_on_candidates vals = { 'remaining_qty': move.remaining_qty + -qty_to_take_on_candidates, 'remaining_value': move.remaining_value + negative_stock_value, 'value': -tmp_value + negative_stock_value, 'price_unit': (-tmp_value + negative_stock_value) / (move.product_qty or quantity), } move.write(vals) return tmp_value def _run_valuation(self, quantity=None): self.ensure_one() if self._is_in(): valued_move_lines = self.move_line_ids.filtered( lambda ml: not ml.location_id._should_be_valued() and ml. location_dest_id._should_be_valued() and not ml.owner_id) valued_quantity = 0 for valued_move_line in valued_move_lines: valued_quantity += valued_move_line.product_uom_id._compute_quantity( valued_move_line.qty_done, self.product_id.uom_id) # Note: we always compute the fifo `remaining_value` and `remaining_qty` fields no # matter which cost method is set, to ease the switching of cost method. vals = {} price_unit = self._get_price_unit() value = price_unit * (quantity or valued_quantity) vals = { 'price_unit': price_unit, 'value': value if quantity is None or not self.value else self.value, 'remaining_value': value if quantity is None else self.remaining_value + value, } vals[ 'remaining_qty'] = valued_quantity if quantity is None else self.remaining_qty + quantity if self.product_id.cost_method == 'standard': value = self.product_id.standard_price * (quantity or valued_quantity) vals.update({ 'price_unit': self.product_id.standard_price, 'value': value if quantity is None or not self.value else self.value, }) self.write(vals) elif self._is_out(): valued_move_lines = self.move_line_ids.filtered( lambda ml: ml.location_id._should_be_valued() and not ml. location_dest_id._should_be_valued() and not ml.owner_id) valued_quantity = sum(valued_move_lines.mapped('qty_done')) self.env['stock.move']._run_fifo(self, quantity=quantity) if self.product_id.cost_method in ['standard', 'average']: curr_rounding = self.company_id.currency_id.rounding value = -float_round( self.product_id.standard_price * (valued_quantity if quantity is None else quantity), precision_rounding=curr_rounding) self.write({ 'value': value if quantity is None else self.value + value, 'price_unit': value / valued_quantity, }) elif self._is_dropshipped(): curr_rounding = self.company_id.currency_id.rounding if self.product_id.cost_method in ['fifo']: price_unit = self._get_price_unit() # see test_dropship_fifo_perpetual_anglosaxon_ordered self.product_id.standard_price = price_unit else: price_unit = self.product_id.standard_price value = float_round(self.product_qty * price_unit, precision_rounding=curr_rounding) # In move have a positive value, out move have a negative value, let's arbitrary say # dropship are positive. self.write({ 'value': value, 'price_unit': price_unit, }) def _action_done(self): self.product_price_update_before_done() res = super(StockMove, self)._action_done() for move in res: # Apply restrictions on the stock move to be able to make # consistent accounting entries. if move._is_in() and move._is_out(): raise UserError( _("The move lines are not in a consistent state: some are entering and other are leaving the company. " )) company_src = move.mapped('move_line_ids.location_id.company_id') company_dst = move.mapped( 'move_line_ids.location_dest_id.company_id') try: if company_src: company_src.ensure_one() if company_dst: company_dst.ensure_one() except ValueError: raise UserError( _("The move lines are not in a consistent states: they do not share the same origin or destination company." )) if company_src and company_dst and company_src.id != company_dst.id: raise UserError( _("The move lines are not in a consistent states: they are doing an intercompany in a single step while they should go through the intercompany transit location." )) move._run_valuation() for move in res.filtered( lambda m: m.product_id.valuation == 'real_time' and (m._is_in() or m._is_out() or m._is_dropshipped())): move._account_entry_move() return res @api.multi def product_price_update_before_done(self, forced_qty=None): tmpl_dict = defaultdict(lambda: 0.0) # adapt standard price on incomming moves if the product cost_method is 'average' std_price_update = {} for move in self.filtered(lambda move: move.location_id.usage in ('supplier', 'production') and move. product_id.cost_method == 'average'): product_tot_qty_available = move.product_id.qty_available + tmpl_dict[ move.product_id.id] rounding = move.product_id.uom_id.rounding if float_is_zero(product_tot_qty_available, precision_rounding=rounding): new_std_price = move._get_price_unit() elif float_is_zero(product_tot_qty_available + move.product_qty, precision_rounding=rounding): new_std_price = move._get_price_unit() else: # Get the standard price amount_unit = std_price_update.get( (move.company_id.id, move.product_id.id)) or move.product_id.standard_price qty = forced_qty or move.product_qty new_std_price = ( (amount_unit * product_tot_qty_available) + (move._get_price_unit() * qty)) / ( product_tot_qty_available + move.product_qty) tmpl_dict[move.product_id.id] += move.product_qty # Write the standard price, as SUPERUSER_ID because a warehouse manager may not have the right to write on products move.product_id.with_context( force_company=move.company_id.id).sudo().write( {'standard_price': new_std_price}) std_price_update[move.company_id.id, move.product_id.id] = new_std_price @api.model def _fifo_vacuum(self): """ Every moves that need to be fixed are identifiable by having a negative `remaining_qty`. """ for move in self.filtered( lambda m: (m._is_in() or m._is_out()) and m.remaining_qty < 0): domain = [('remaining_qty', '>', 0), '|', ('date', '>', move.date), '&', ('date', '=', move.date), ('id', '>', move.id)] domain += move._get_in_domain() candidates = self.search(domain, order='date, id') if not candidates: continue qty_to_take_on_candidates = abs(move.remaining_qty) tmp_value = 0 for candidate in candidates: if candidate.remaining_qty <= qty_to_take_on_candidates: qty_taken_on_candidate = candidate.remaining_qty else: qty_taken_on_candidate = qty_to_take_on_candidates value_taken_on_candidate = qty_taken_on_candidate * candidate.price_unit candidate_vals = { 'remaining_qty': candidate.remaining_qty - qty_taken_on_candidate, 'remaining_value': candidate.remaining_value - value_taken_on_candidate, } candidate.write(candidate_vals) qty_to_take_on_candidates -= qty_taken_on_candidate tmp_value += value_taken_on_candidate if qty_to_take_on_candidates == 0: break remaining_value_before_vacuum = move.remaining_value # If `remaining_qty` should be updated to 0, we wipe `remaining_value`. If it was set # it was only used to infer the correction entry anyway. new_remaining_qty = -qty_to_take_on_candidates new_remaining_value = 0 if not new_remaining_qty else move.remaining_value + tmp_value move.write({ 'remaining_value': new_remaining_value, 'remaining_qty': new_remaining_qty, }) if move.product_id.valuation == 'real_time': # If `move.remaining_value` is negative, it means that we initially valued this move at # an estimated price *and* posted an entry. `tmp_value` is the real value we took to # compensate and should always be positive, but if the remaining value is still negative # we have to take care to not overvalue by decreasing the correction entry by what's # already been posted. corrected_value = tmp_value if remaining_value_before_vacuum < 0: corrected_value += remaining_value_before_vacuum # If `corrected_value` is 0, absolutely do *not* call `_account_entry_move`. We # force the amount in the context, but in the case it is 0 it'll create an entry # for the entire cost of the move. This case happens when the candidates moves # entirely compensate the problematic move. if not corrected_value: continue if move._is_in(): # If we just compensated an IN move that has a negative remaining # quantity, it means the move has returned more items than it received. # The correction should behave as a return too. As `_account_entry_move` # will post the natural values for an IN move (credit IN account, debit # OUT one), we inverse the sign to create the correct entries. move.with_context(force_valuation_amount=-corrected_value )._account_entry_move() else: move.with_context(force_valuation_amount=corrected_value )._account_entry_move() @api.model def _run_fifo_vacuum(self): # Call `_fifo_vacuum` on concerned moves fifo_valued_products = self.env['product.product'] fifo_valued_products |= self.env['product.template'].search([ ('property_cost_method', '=', 'fifo') ]).mapped('product_variant_ids') fifo_valued_categories = self.env['product.category'].search([ ('property_cost_method', '=', 'fifo') ]) fifo_valued_products |= self.env['product.product'].search([ ('categ_id', 'child_of', fifo_valued_categories.ids) ]) moves_to_vacuum = self.env['stock.move'] for product in fifo_valued_products: moves_to_vacuum |= self.search([('product_id', '=', product.id), ('remaining_qty', '<', 0)] + self._get_all_base_domain()) moves_to_vacuum._fifo_vacuum() @api.multi def _get_accounting_data_for_valuation(self): """ Return the accounts and journal to use to post Journal Entries for the real-time valuation of the quant. """ self.ensure_one() accounts_data = self.product_id.product_tmpl_id.get_product_accounts() if self.location_id.valuation_out_account_id: acc_src = self.location_id.valuation_out_account_id.id else: acc_src = accounts_data['stock_input'].id if self.location_dest_id.valuation_in_account_id: acc_dest = self.location_dest_id.valuation_in_account_id.id else: acc_dest = accounts_data['stock_output'].id acc_valuation = accounts_data.get('stock_valuation', False) if acc_valuation: acc_valuation = acc_valuation.id if not accounts_data.get('stock_journal', False): raise UserError( _('You don\'t have any stock journal defined on your product category, check if you have installed a chart of accounts' )) if not acc_src: raise UserError( _('Cannot find a stock input account for the product %s. You must define one on the product category, or on the location, before processing this operation.' ) % (self.product_id.name)) if not acc_dest: raise UserError( _('Cannot find a stock output account for the product %s. You must define one on the product category, or on the location, before processing this operation.' ) % (self.product_id.name)) if not acc_valuation: raise UserError( _('You don\'t have any stock valuation account defined on your product category. You must define one before processing this operation.' )) journal_id = accounts_data['stock_journal'].id return journal_id, acc_src, acc_dest, acc_valuation def _prepare_account_move_line(self, qty, cost, credit_account_id, debit_account_id): """ Generate the account.move.line values to post to track the stock valuation difference due to the processing of the given quant. """ self.ensure_one() if self._context.get('force_valuation_amount'): valuation_amount = self._context.get('force_valuation_amount') else: valuation_amount = cost # the standard_price of the product may be in another decimal precision, or not compatible with the coinage of # the company currency... so we need to use round() before creating the accounting entries. debit_value = self.company_id.currency_id.round(valuation_amount) # check that all data is correct if self.company_id.currency_id.is_zero(debit_value): raise UserError( _("The cost of %s is currently equal to 0. Change the cost or the configuration of your product to avoid an incorrect valuation." ) % (self.product_id.name, )) credit_value = debit_value if self.product_id.cost_method == 'average' and self.company_id.anglo_saxon_accounting: # in case of a supplier return in anglo saxon mode, for products in average costing method, the stock_input # account books the real purchase price, while the stock account books the average price. The difference is # booked in the dedicated price difference account. if self.location_dest_id.usage == 'supplier' and self.origin_returned_move_id and self.origin_returned_move_id.purchase_line_id: debit_value = self.origin_returned_move_id.price_unit * qty # in case of a customer return in anglo saxon mode, for products in average costing method, the stock valuation # is made using the original average price to negate the delivery effect. if self.location_id.usage == 'customer' and self.origin_returned_move_id: debit_value = self.origin_returned_move_id.price_unit * qty credit_value = debit_value partner_id = (self.picking_id.partner_id and self.env['res.partner']._find_accounting_partner( self.picking_id.partner_id).id) or False debit_line_vals = { 'name': self.name, 'product_id': self.product_id.id, 'quantity': qty, 'product_uom_id': self.product_id.uom_id.id, 'ref': self.picking_id.name, 'partner_id': partner_id, 'debit': debit_value if debit_value > 0 else 0, 'credit': -debit_value if debit_value < 0 else 0, 'account_id': debit_account_id, } credit_line_vals = { 'name': self.name, 'product_id': self.product_id.id, 'quantity': qty, 'product_uom_id': self.product_id.uom_id.id, 'ref': self.picking_id.name, 'partner_id': partner_id, 'credit': credit_value if credit_value > 0 else 0, 'debit': -credit_value if credit_value < 0 else 0, 'account_id': credit_account_id, } res = [(0, 0, debit_line_vals), (0, 0, credit_line_vals)] if credit_value != debit_value: # for supplier returns of product in average costing method, in anglo saxon mode diff_amount = debit_value - credit_value price_diff_account = self.product_id.property_account_creditor_price_difference if not price_diff_account: price_diff_account = self.product_id.categ_id.property_account_creditor_price_difference_categ if not price_diff_account: raise UserError( _('Configuration error. Please configure the price difference account on the product or its category to process this operation.' )) price_diff_line = { 'name': self.name, 'product_id': self.product_id.id, 'quantity': qty, 'product_uom_id': self.product_id.uom_id.id, 'ref': self.picking_id.name, 'partner_id': partner_id, 'credit': diff_amount > 0 and diff_amount or 0, 'debit': diff_amount < 0 and -diff_amount or 0, 'account_id': price_diff_account.id, } res.append((0, 0, price_diff_line)) return res def _create_account_move_line(self, credit_account_id, debit_account_id, journal_id): self.ensure_one() AccountMove = self.env['account.move'] move_lines = self._prepare_account_move_line(self.product_qty, abs(self.value), credit_account_id, debit_account_id) if move_lines: date = self._context.get('force_period_date', fields.Date.context_today(self)) new_account_move = AccountMove.create({ 'journal_id': journal_id, 'line_ids': move_lines, 'date': date, 'ref': self.picking_id.name, 'stock_move_id': self.id, }) new_account_move.post() def _account_entry_move(self): """ Accounting Valuation Entries """ self.ensure_one() if self.product_id.type != 'product': # no stock valuation for consumable products return False if self.restrict_partner_id: # if the move isn't owned by the company, we don't make any valuation return False location_from = self.location_id location_to = self.location_dest_id company_from = self._is_out() and self.mapped( 'move_line_ids.location_id.company_id') or False company_to = self._is_in() and self.mapped( 'move_line_ids.location_dest_id.company_id') or False # Create Journal Entry for products arriving in the company; in case of routes making the link between several # warehouse of the same company, the transit location belongs to this company, so we don't need to create accounting entries if self._is_in(): journal_id, acc_src, acc_dest, acc_valuation = self._get_accounting_data_for_valuation( ) if location_from and location_from.usage == 'customer': # goods returned from customer self.with_context( force_company=company_to.id)._create_account_move_line( acc_dest, acc_valuation, journal_id) else: self.with_context( force_company=company_to.id)._create_account_move_line( acc_src, acc_valuation, journal_id) # Create Journal Entry for products leaving the company if self._is_out(): journal_id, acc_src, acc_dest, acc_valuation = self._get_accounting_data_for_valuation( ) if location_to and location_to.usage == 'supplier': # goods returned to supplier self.with_context( force_company=company_from.id)._create_account_move_line( acc_valuation, acc_src, journal_id) else: self.with_context( force_company=company_from.id)._create_account_move_line( acc_valuation, acc_dest, journal_id) if self.company_id.anglo_saxon_accounting and self._is_dropshipped(): # Creates an account entry from stock_input to stock_output on a dropship move. https://github.com/odoo/odoo/issues/12687 journal_id, acc_src, acc_dest, acc_valuation = self._get_accounting_data_for_valuation( ) self.with_context( force_company=self.company_id.id)._create_account_move_line( acc_src, acc_dest, journal_id)
class MaintenanceRequest(models.Model): _name = 'maintenance.request' _inherit = ['mail.thread', 'mail.activity.mixin'] _description = 'Maintenance Requests' _order = "id desc" @api.returns('self') def _default_stage(self): return self.env['maintenance.stage'].search([], limit=1) @api.multi def _track_subtype(self, init_values): self.ensure_one() if 'stage_id' in init_values and self.stage_id.sequence <= 1: return 'maintenance.mt_req_created' elif 'stage_id' in init_values and self.stage_id.sequence > 1: return 'maintenance.mt_req_status' return super(MaintenanceRequest, self)._track_subtype(init_values) def _get_default_team_id(self): return self.env.ref('maintenance.equipment_team_maintenance', raise_if_not_found=False) name = fields.Char('Subjects', required=True) description = fields.Text('Description') 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) technician_user_id = fields.Many2one('res.users', string='Owner', track_visibility='onchange', oldname='user_id') stage_id = fields.Many2one('maintenance.stage', string='Stage', track_visibility='onchange', 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', 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', 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.") @api.multi def archive_equipment_request(self): self.write({'archive': True}) @api.multi def reset_equipment_request(self): """ Reinsert the maintenance request into the maintenance pipe in the first stage""" first_stage_obj = self.env['maintenance.stage'].search([], order="sequence asc", limit=1) # self.write({'active': True, 'stage_id': first_stage_obj.id}) self.write({'archive': False, 'stage_id': first_stage_obj.id}) @api.onchange('equipment_id') def onchange_equipment_id(self): if self.equipment_id: self.technician_user_id = self.equipment_id.technician_user_id if self.equipment_id.technician_user_id else self.equipment_id.category_id.technician_user_id self.category_id = self.equipment_id.category_id if self.equipment_id.maintenance_team_id: self.maintenance_team_id = self.equipment_id.maintenance_team_id.id @api.onchange('category_id') def onchange_category_id(self): if not self.technician_user_id or not self.equipment_id or (self.technician_user_id and not self.equipment_id.technician_user_id): self.technician_user_id = self.category_id.technician_user_id @api.model def create(self, vals): # context: no_log, because subtype already handle this self = self.with_context(mail_create_nolog=True) request = super(MaintenanceRequest, self).create(vals) if request.owner_user_id or request.technician_user_id: request._add_followers() if request.equipment_id and not request.maintenance_team_id: request.maintenance_team_id = request.equipment_id.maintenance_team_id return request @api.multi def write(self, vals): # Overridden to reset the kanban_state to normal whenever # the stage (stage_id) of the Maintenance Request changes. if vals and 'kanban_state' not in vals and 'stage_id' in vals: vals['kanban_state'] = 'normal' res = super(MaintenanceRequest, self).write(vals) if vals.get('owner_user_id') or vals.get('technician_user_id'): self._add_followers() if self.stage_id.done and 'stage_id' in vals: self.write({'close_date': fields.Date.today()}) return res def _add_followers(self): for request in self: user_ids = (request.owner_user_id + request.technician_user_id).ids request.message_subscribe_users(user_ids=user_ids) @api.model def _read_group_stage_ids(self, stages, domain, order): """ Read group customization in order to display all the stages in the kanban view, even if they are empty """ stage_ids = stages._search([], order=order, access_rights_uid=SUPERUSER_ID) return stages.browse(stage_ids)
class IrFilters(models.Model): _name = 'ir.filters' _description = 'Filters' _order = 'model_id, name, id desc' name = fields.Char(string='Filter Name', translate=True, required=True) user_id = fields.Many2one('res.users', string='User', ondelete='cascade', default=lambda self: self._uid, help="The user this filter is private to. When left empty the filter is public " "and available to all users.") domain = fields.Text(default='[]', required=True) context = fields.Text(default='{}', required=True) sort = fields.Text(default='[]', required=True) model_id = fields.Selection(selection='_list_all_models', string='Model', required=True) is_default = fields.Boolean(string='Default Filter') action_id = fields.Many2one('ir.actions.actions', string='Action', ondelete='cascade', help="The menu action this filter applies to. " "When left empty the filter applies to all menus " "for this model.") active = fields.Boolean(default=True) @api.model def _list_all_models(self): self._cr.execute("SELECT model, name FROM ir_model ORDER BY name") return self._cr.fetchall() @api.multi def copy(self, default=None): self.ensure_one() default = dict(default or {}, name=_('%s (copy)') % self.name) return super(IrFilters, self).copy(default) @api.multi def _get_eval_domain(self): self.ensure_one() return ast.literal_eval(self.domain) @api.model def _get_action_domain(self, action_id=None): """Return a domain component for matching filters that are visible in the same context (menu/view) as the given action.""" if action_id: # filters specific to this menu + global ones return [('action_id', 'in', [action_id, False])] # only global ones return [('action_id', '=', False)] @api.model def get_filters(self, model, action_id=None): """Obtain the list of filters available for the user on the given model. :param action_id: optional ID of action to restrict filters to this action plus global filters. If missing only global filters are returned. The action does not have to correspond to the model, it may only be a contextual action. :return: list of :meth:`~osv.read`-like dicts containing the ``name``, ``is_default``, ``domain``, ``user_id`` (m2o tuple), ``action_id`` (m2o tuple) and ``context`` of the matching ``ir.filters``. """ # available filters: private filters (user_id=uid) and public filters (uid=NULL), # and filters for the action (action_id=action_id) or global (action_id=NULL) action_domain = self._get_action_domain(action_id) filters = self.search(action_domain + [('model_id', '=', model), ('user_id', 'in', [self._uid, False])]) user_context = self.env.user.context_get() return filters.with_context(user_context).read(['name', 'is_default', 'domain', 'context', 'user_id', 'sort']) @api.model def _check_global_default(self, vals, matching_filters): """ _check_global_default(dict, list(dict), dict) -> None Checks if there is a global default for the model_id requested. If there is, and the default is different than the record being written (-> we're not updating the current global default), raise an error to avoid users unknowingly overwriting existing global defaults (they have to explicitly remove the current default before setting a new one) This method should only be called if ``vals`` is trying to set ``is_default`` :raises actpy.exceptions.UserError: if there is an existing default and we're not updating it """ domain = self._get_action_domain(vals.get('action_id')) defaults = self.search(domain + [ ('model_id', '=', vals['model_id']), ('user_id', '=', False), ('is_default', '=', True), ]) if not defaults: return if matching_filters and (matching_filters[0]['id'] == defaults.id): return raise UserError(_("There is already a shared filter set as default for %(model)s, delete or change it before setting a new default") % {'model': vals.get('model_id')}) @api.model @api.returns('self', lambda value: value.id) def create_or_replace(self, vals): action_id = vals.get('action_id') current_filters = self.get_filters(vals['model_id'], action_id) matching_filters = [f for f in current_filters if f['name'].lower() == vals['name'].lower() # next line looks for matching user_ids (specific or global), i.e. # f.user_id is False and vals.user_id is False or missing, # or f.user_id.id == vals.user_id if (f['user_id'] and f['user_id'][0]) == vals.get('user_id')] if vals.get('is_default'): if vals.get('user_id'): # Setting new default: any other default that belongs to the user # should be turned off domain = self._get_action_domain(action_id) defaults = self.search(domain + [ ('model_id', '=', vals['model_id']), ('user_id', '=', vals['user_id']), ('is_default', '=', True), ]) if defaults: defaults.write({'is_default': False}) else: self._check_global_default(vals, matching_filters) # When a filter exists for the same (name, model, user) triple, we simply # replace its definition (considering action_id irrelevant here) if matching_filters: matching_filter = self.browse(matching_filters[0]['id']) matching_filter.write(vals) return matching_filter return self.create(vals) _sql_constraints = [ # Partial constraint, complemented by unique index (see below). Still # useful to keep because it provides a proper error message when a # violation occurs, as it shares the same prefix as the unique index. ('name_model_uid_unique', 'unique (name, model_id, user_id, action_id)', 'Filter names must be unique'), ] @api.model_cr_context def _auto_init(self): result = super(IrFilters, self)._auto_init() # Use unique index to implement unique constraint on the lowercase name (not possible using a constraint) tools.create_unique_index(self._cr, 'ir_filters_name_model_uid_unique_action_index', self._table, ['lower(name)', 'model_id', 'COALESCE(user_id,-1)', 'COALESCE(action_id,-1)']) return result
class ResConfigSettings(models.TransientModel): _inherit = 'res.config.settings' def _default_order_mail_template(self): if self.env['ir.module.module'].search([ ('name', '=', 'website_quote') ]).state in ('installed', 'to upgrade'): return self.env.ref('website_quote.confirmation_mail').id else: return self.env.ref('sale.email_template_edi_sale').id def _default_recovery_mail_template(self): try: return self.env.ref( 'website_sale.mail_template_sale_cart_recovery').id except ValueError: return False salesperson_id = fields.Many2one('res.users', related='website_id.salesperson_id', string='Salesperson') salesteam_id = fields.Many2one('crm.team', related='website_id.salesteam_id', string='Sales Channel', domain=[('team_type', '!=', 'pos')]) module_website_sale_delivery = fields.Boolean("Shipping Costs") # field used to have a nice radio in form view, resuming the 2 fields above sale_delivery_settings = fields.Selection([ ('none', 'No shipping management on website'), ('internal', "Delivery methods are only used internally: the customer doesn't pay for shipping costs" ), ('website', "Delivery methods are selectable on the website: the customer pays for shipping costs" ), ], string="Shipping Management") group_website_multiimage = fields.Boolean( string='Multi-Images', implied_group='website_sale.group_website_multi_image', group='base.group_portal,base.group_user,base.group_public') group_delivery_invoice_address = fields.Boolean( string="Shipping Address", implied_group='sale.group_delivery_invoice_address') module_website_sale_options = fields.Boolean("Optional Products") module_website_sale_digital = fields.Boolean("Digital Content") module_website_sale_wishlist = fields.Boolean("Wishlists") module_website_sale_comparison = fields.Boolean("Product Comparison Tool") module_website_sale_stock = fields.Boolean( "Inventory", help='Installs *e-Commerce Inventory*') module_account_invoicing = fields.Boolean("Invoicing") order_mail_template = fields.Many2one( 'mail.template', string='Order Confirmation Email', default=_default_order_mail_template, domain="[('model', '=', 'sale.order')]", help="Email sent to customer at the end of the checkout process") automatic_invoice = fields.Boolean("Automatic Invoice") module_l10n_eu_service = fields.Boolean(string="EU Digital Goods VAT") cart_recovery_mail_template = fields.Many2one( 'mail.template', string='Cart Recovery Email', default=_default_recovery_mail_template, domain="[('model', '=', 'sale.order')]") cart_abandoned_delay = fields.Float( "Abandoned Delay", default=1.0, help="number of hours after which the cart is considered abandoned") @api.model def get_values(self): res = super(ResConfigSettings, self).get_values() params = self.env['ir.config_parameter'].sudo() sale_delivery_settings = 'none' if self.env['ir.module.module'].search( [('name', '=', 'delivery')], limit=1).state in ('installed', 'to install', 'to upgrade'): sale_delivery_settings = 'internal' if self.env['ir.module.module'].search( [('name', '=', 'website_sale_delivery')], limit=1).state in ('installed', 'to install', 'to upgrade'): sale_delivery_settings = 'website' cart_recovery_mail_template = literal_eval( params.get_param('website_sale.cart_recovery_mail_template_id', default='False')) if cart_recovery_mail_template and not self.env[ 'mail.template'].browse(cart_recovery_mail_template).exists(): cart_recovery_mail_template = self._default_recovery_mail_template( ) res.update(automatic_invoice=params.get_param( 'website_sale.automatic_invoice', default=False), sale_delivery_settings=sale_delivery_settings, cart_recovery_mail_template=cart_recovery_mail_template, cart_abandoned_delay=float( params.get_param('website_sale.cart_abandoned_delay', '1.0'))) return res def set_values(self): super(ResConfigSettings, self).set_values() value = self.module_account_invoicing and self.default_invoice_policy == 'order' and self.automatic_invoice self.env['ir.config_parameter'].sudo().set_param( 'website_sale.automatic_invoice', value) self.env['ir.config_parameter'].sudo().set_param( 'website_sale.cart_recovery_mail_template_id', self.cart_recovery_mail_template.id) self.env['ir.config_parameter'].sudo().set_param( 'website_sale.cart_abandoned_delay', self.cart_abandoned_delay) @api.onchange('sale_delivery_settings') def _onchange_sale_delivery_settings(self): if self.sale_delivery_settings == 'none': self.update({ 'module_delivery': False, 'module_website_sale_delivery': False, }) elif self.sale_delivery_settings == 'internal': self.update({ 'module_delivery': True, 'module_website_sale_delivery': False, }) else: self.update({ 'module_delivery': True, 'module_website_sale_delivery': True, }) @api.onchange('group_discount_per_so_line') def _onchange_group_discount_per_so_line(self): if self.group_discount_per_so_line: self.update({ 'multi_sales_price': True, })
class ResConfigSettings(models.TransientModel): _inherit = 'res.config.settings' group_attendance_use_pin = fields.Boolean( string='Employee PIN', implied_group="hr_attendance.group_hr_attendance_use_pin")
class AccountAssetDepreciationLine(models.Model): _name = 'account.asset.depreciation.line' _description = 'Asset depreciation line' name = fields.Char(string='Depreciation Name', required=True, index=True) sequence = fields.Integer(required=True) asset_id = fields.Many2one('account.asset.asset', string='Asset', required=True, ondelete='cascade') parent_state = fields.Selection(related='asset_id.state', string='State of Asset') amount = fields.Float(string='Current Depreciation', digits=0, required=True) remaining_value = fields.Float(string='Next Period Depreciation', digits=0, required=True) depreciated_value = fields.Float(string='Cumulative Depreciation', required=True) depreciation_date = fields.Date('Depreciation Date', index=True) move_id = fields.Many2one('account.move', string='Depreciation Entry') move_check = fields.Boolean(compute='_get_move_check', string='Linked', track_visibility='always', store=True) move_posted_check = fields.Boolean(compute='_get_move_posted_check', string='Posted', track_visibility='always', store=True) @api.multi @api.depends('move_id') def _get_move_check(self): for line in self: line.move_check = bool(line.move_id) @api.multi @api.depends('move_id.state') def _get_move_posted_check(self): for line in self: line.move_posted_check = True if line.move_id and line.move_id.state == 'posted' else False @api.multi def create_move(self, post_move=True): created_moves = self.env['account.move'] prec = self.env['decimal.precision'].precision_get('Account') for line in self: if line.move_id: raise UserError(_('This depreciation is already linked to a journal entry! Please post or delete it.')) category_id = line.asset_id.category_id depreciation_date = self.env.context.get('depreciation_date') or line.depreciation_date or fields.Date.context_today(self) company_currency = line.asset_id.company_id.currency_id current_currency = line.asset_id.currency_id amount = current_currency.with_context(date=depreciation_date).compute(line.amount, company_currency) asset_name = line.asset_id.name + ' (%s/%s)' % (line.sequence, len(line.asset_id.depreciation_line_ids)) move_line_1 = { 'name': asset_name, 'account_id': category_id.account_depreciation_id.id, 'debit': 0.0 if float_compare(amount, 0.0, precision_digits=prec) > 0 else -amount, 'credit': amount if float_compare(amount, 0.0, precision_digits=prec) > 0 else 0.0, 'journal_id': category_id.journal_id.id, 'partner_id': line.asset_id.partner_id.id, 'analytic_account_id': category_id.account_analytic_id.id if category_id.type == 'sale' else False, 'currency_id': company_currency != current_currency and current_currency.id or False, 'amount_currency': company_currency != current_currency and - 1.0 * line.amount or 0.0, } move_line_2 = { 'name': asset_name, 'account_id': category_id.account_depreciation_expense_id.id, 'credit': 0.0 if float_compare(amount, 0.0, precision_digits=prec) > 0 else -amount, 'debit': amount if float_compare(amount, 0.0, precision_digits=prec) > 0 else 0.0, 'journal_id': category_id.journal_id.id, 'partner_id': line.asset_id.partner_id.id, 'analytic_account_id': category_id.account_analytic_id.id if category_id.type == 'purchase' else False, 'currency_id': company_currency != current_currency and current_currency.id or False, 'amount_currency': company_currency != current_currency and line.amount or 0.0, } move_vals = { 'ref': line.asset_id.code, 'date': depreciation_date or False, 'journal_id': category_id.journal_id.id, 'line_ids': [(0, 0, move_line_1), (0, 0, move_line_2)], } move = self.env['account.move'].create(move_vals) line.write({'move_id': move.id, 'move_check': True}) created_moves |= move if post_move and created_moves: created_moves.filtered(lambda m: any(m.asset_depreciation_ids.mapped('asset_id.category_id.open_asset'))).post() return [x.id for x in created_moves] @api.multi def create_grouped_move(self, post_move=True): if not self.exists(): return [] created_moves = self.env['account.move'] category_id = self[0].asset_id.category_id # we can suppose that all lines have the same category depreciation_date = self.env.context.get('depreciation_date') or fields.Date.context_today(self) amount = 0.0 for line in self: # Sum amount of all depreciation lines company_currency = line.asset_id.company_id.currency_id current_currency = line.asset_id.currency_id amount += current_currency.compute(line.amount, company_currency) name = category_id.name + _(' (grouped)') move_line_1 = { 'name': name, 'account_id': category_id.account_depreciation_id.id, 'debit': 0.0, 'credit': amount, 'journal_id': category_id.journal_id.id, 'analytic_account_id': category_id.account_analytic_id.id if category_id.type == 'sale' else False, } move_line_2 = { 'name': name, 'account_id': category_id.account_depreciation_expense_id.id, 'credit': 0.0, 'debit': amount, 'journal_id': category_id.journal_id.id, 'analytic_account_id': category_id.account_analytic_id.id if category_id.type == 'purchase' else False, } move_vals = { 'ref': category_id.name, 'date': depreciation_date or False, 'journal_id': category_id.journal_id.id, 'line_ids': [(0, 0, move_line_1), (0, 0, move_line_2)], } move = self.env['account.move'].create(move_vals) self.write({'move_id': move.id, 'move_check': True}) created_moves |= move if post_move and created_moves: self.post_lines_and_close_asset() created_moves.post() return [x.id for x in created_moves] @api.multi def post_lines_and_close_asset(self): # we re-evaluate the assets to determine whether we can close them for line in self: line.log_message_when_posted() asset = line.asset_id if asset.currency_id.is_zero(asset.value_residual): asset.message_post(body=_("Document closed.")) asset.write({'state': 'close'}) @api.multi def log_message_when_posted(self): def _format_message(message_description, tracked_values): message = '' if message_description: message = '<span>%s</span>' % message_description for name, values in tracked_values.items(): message += '<div> • <b>%s</b>: ' % name message += '%s</div>' % values return message for line in self: if line.move_id and line.move_id.state == 'draft': partner_name = line.asset_id.partner_id.name currency_name = line.asset_id.currency_id.name msg_values = {_('Currency'): currency_name, _('Amount'): line.amount} if partner_name: msg_values[_('Partner')] = partner_name msg = _format_message(_('Depreciation line posted.'), msg_values) line.asset_id.message_post(body=msg) @api.multi def unlink(self): for record in self: if record.move_check: if record.asset_id.category_id.type == 'purchase': msg = _("You cannot delete posted depreciation lines.") else: msg = _("You cannot delete posted installment lines.") raise UserError(msg) return super(AccountAssetDepreciationLine, self).unlink()
class Note(models.Model): _name = 'note.note' _inherit = ['mail.thread', 'mail.activity.mixin'] _description = "Note" _order = 'sequence' def _get_default_stage_id(self): return self.env['note.stage'].search([('user_id', '=', self.env.uid)], limit=1) name = fields.Text(compute='_compute_name', string='Note Summary', store=True) user_id = fields.Many2one('res.users', string='Owner', default=lambda self: self.env.uid) memo = fields.Html('Note Content') sequence = fields.Integer('Sequence') stage_id = fields.Many2one('note.stage', compute='_compute_stage_id', inverse='_inverse_stage_id', string='Stage') stage_ids = fields.Many2many('note.stage', 'note_stage_rel', 'note_id', 'stage_id', string='Stages of Users', default=_get_default_stage_id) open = fields.Boolean(string='Active', default=True) date_done = fields.Date('Date done') color = fields.Integer(string='Color Index') tag_ids = fields.Many2many('note.tag', 'note_tags_rel', 'note_id', 'tag_id', string='Tags') @api.depends('memo') def _compute_name(self): """ Read the first line of the memo to determine the note name """ for note in self: text = html2plaintext(note.memo) if note.memo else '' note.name = text.strip().replace('*', '').split("\n")[0] @api.multi def _compute_stage_id(self): for note in self: for stage in note.stage_ids.filtered(lambda stage: stage.user_id == self.env.user): note.stage_id = stage @api.multi def _inverse_stage_id(self): for note in self.filtered('stage_id'): note.stage_ids = note.stage_id + note.stage_ids.filtered(lambda stage: stage.user_id != self.env.user) @api.model def name_create(self, name): return self.create({'memo': name}).name_get()[0] @api.model def read_group(self, domain, fields, groupby, offset=0, limit=None, orderby=False, lazy=True): if groupby and groupby[0] == "stage_id": stages = self.env['note.stage'].search([('user_id', '=', self.env.uid)]) if stages: # if the user has some stages result = [{ # notes by stage for stages user '__context': {'group_by': groupby[1:]}, '__domain': domain + [('stage_ids.id', '=', stage.id)], 'stage_id': (stage.id, stage.name), 'stage_id_count': self.search_count(domain + [('stage_ids', '=', stage.id)]), '__fold': stage.fold, } for stage in stages] # note without user's stage nb_notes_ws = self.search_count(domain + [('stage_ids', 'not in', stages.ids)]) if nb_notes_ws: # add note to the first column if it's the first stage dom_not_in = ('stage_ids', 'not in', stages.ids) if result and result[0]['stage_id'][0] == stages[0].id: dom_in = result[0]['__domain'].pop() result[0]['__domain'] = domain + ['|', dom_in, dom_not_in] result[0]['stage_id_count'] += nb_notes_ws else: # add the first stage column result = [{ '__context': {'group_by': groupby[1:]}, '__domain': domain + [dom_not_in], 'stage_id': (stages[0].id, stages[0].name), 'stage_id_count': nb_notes_ws, '__fold': stages[0].name, }] + result else: # if stage_ids is empty, get note without user's stage nb_notes_ws = self.search_count(domain) if nb_notes_ws: result = [{ # notes for unknown stage '__context': {'group_by': groupby[1:]}, '__domain': domain, 'stage_id': False, 'stage_id_count': nb_notes_ws }] else: result = [] return result return super(Note, self).read_group(domain, fields, groupby, offset=offset, limit=limit, orderby=orderby, lazy=lazy) @api.multi def action_close(self): return self.write({'open': False, 'date_done': fields.date.today()}) @api.multi def action_open(self): return self.write({'open': True})
class Post(models.Model): _name = 'forum.post' _description = 'Forum Post' _inherit = ['mail.thread', 'website.seo.metadata'] _order = "is_correct DESC, vote_count DESC, write_date DESC" name = fields.Char('Title') forum_id = fields.Many2one('forum.forum', string='Forum', required=True) content = fields.Html('Content', strip_style=True) plain_content = fields.Text('Plain Content', compute='_get_plain_content', store=True) content_link = fields.Char('URL', help="URL of Link Articles") tag_ids = fields.Many2many('forum.tag', 'forum_tag_rel', 'forum_id', 'forum_tag_id', string='Tags') state = fields.Selection([('active', 'Active'), ('pending', 'Waiting Validation'), ('close', 'Close'), ('offensive', 'Offensive'), ('flagged', 'Flagged')], string='Status', default='active') views = fields.Integer('Number of Views', default=0) active = fields.Boolean('Active', default=True) post_type = fields.Selection([ ('question', 'Question'), ('link', 'Article'), ('discussion', 'Discussion')], string='Type', default='question', required=True) website_message_ids = fields.One2many(domain=lambda self: [('model', '=', self._name), ('message_type', 'in', ['email', 'comment'])]) # history create_date = fields.Datetime('Asked on', index=True, readonly=True) create_uid = fields.Many2one('res.users', string='Created by', index=True, readonly=True) write_date = fields.Datetime('Update on', index=True, readonly=True) bump_date = fields.Datetime('Bumped on', readonly=True, help="Technical field allowing to bump a question. Writing on this field will trigger " "a write on write_date and therefore bump the post. Directly writing on write_date " "is currently not supported and this field is a workaround.") write_uid = fields.Many2one('res.users', string='Updated by', index=True, readonly=True) relevancy = fields.Float('Relevance', compute="_compute_relevancy", store=True) # vote vote_ids = fields.One2many('forum.post.vote', 'post_id', string='Votes') user_vote = fields.Integer('My Vote', compute='_get_user_vote') vote_count = fields.Integer('Total Votes', compute='_get_vote_count', store=True) # favorite favourite_ids = fields.Many2many('res.users', string='Favourite') user_favourite = fields.Boolean('Is Favourite', compute='_get_user_favourite') favourite_count = fields.Integer('Favorite Count', compute='_get_favorite_count', store=True) # hierarchy is_correct = fields.Boolean('Correct', help='Correct answer or answer accepted') parent_id = fields.Many2one('forum.post', string='Question', ondelete='cascade') self_reply = fields.Boolean('Reply to own question', compute='_is_self_reply', store=True) child_ids = fields.One2many('forum.post', 'parent_id', string='Answers') child_count = fields.Integer('Number of answers', compute='_get_child_count', store=True) uid_has_answered = fields.Boolean('Has Answered', compute='_get_uid_has_answered') has_validated_answer = fields.Boolean('Is answered', compute='_get_has_validated_answer', store=True) # offensive moderation tools flag_user_id = fields.Many2one('res.users', string='Flagged by') moderator_id = fields.Many2one('res.users', string='Reviewed by', readonly=True) # closing closed_reason_id = fields.Many2one('forum.post.reason', string='Reason') closed_uid = fields.Many2one('res.users', string='Closed by', index=True) closed_date = fields.Datetime('Closed on', readonly=True) # karma calculation and access karma_accept = fields.Integer('Convert comment to answer', compute='_get_post_karma_rights') karma_edit = fields.Integer('Karma to edit', compute='_get_post_karma_rights') karma_close = fields.Integer('Karma to close', compute='_get_post_karma_rights') karma_unlink = fields.Integer('Karma to unlink', compute='_get_post_karma_rights') karma_comment = fields.Integer('Karma to comment', compute='_get_post_karma_rights') karma_comment_convert = fields.Integer('Karma to convert comment to answer', compute='_get_post_karma_rights') karma_flag = fields.Integer('Flag a post as offensive', compute='_get_post_karma_rights') can_ask = fields.Boolean('Can Ask', compute='_get_post_karma_rights') can_answer = fields.Boolean('Can Answer', compute='_get_post_karma_rights') can_accept = fields.Boolean('Can Accept', compute='_get_post_karma_rights') can_edit = fields.Boolean('Can Edit', compute='_get_post_karma_rights') can_close = fields.Boolean('Can Close', compute='_get_post_karma_rights') can_unlink = fields.Boolean('Can Unlink', compute='_get_post_karma_rights') can_upvote = fields.Boolean('Can Upvote', compute='_get_post_karma_rights') can_downvote = fields.Boolean('Can Downvote', compute='_get_post_karma_rights') can_comment = fields.Boolean('Can Comment', compute='_get_post_karma_rights') can_comment_convert = fields.Boolean('Can Convert to Comment', compute='_get_post_karma_rights') can_view = fields.Boolean('Can View', compute='_get_post_karma_rights', search='_search_can_view') can_display_biography = fields.Boolean("Is the author's biography visible from his post", compute='_get_post_karma_rights') can_post = fields.Boolean('Can Automatically be Validated', compute='_get_post_karma_rights') can_flag = fields.Boolean('Can Flag', compute='_get_post_karma_rights') can_moderate = fields.Boolean('Can Moderate', compute='_get_post_karma_rights') website_id = fields.Many2one('website', string="Website", default=lambda self: self.env.ref('website.default_website')) def _search_can_view(self, operator, value): if operator not in ('=', '!=', '<>'): raise ValueError('Invalid operator: %s' % (operator,)) if not value: operator = operator == "=" and '!=' or '=' value = True if self._uid == SUPERUSER_ID: return [(1, '=', 1)] user = self.env['res.users'].browse(self._uid) req = """ SELECT p.id FROM forum_post p LEFT JOIN res_users u ON p.create_uid = u.id LEFT JOIN forum_forum f ON p.forum_id = f.id WHERE (p.create_uid = %s and f.karma_close_own <= %s) or (p.create_uid != %s and f.karma_close_all <= %s) or ( u.karma > 0 and (p.active or p.create_uid = %s) ) """ op = operator == "=" and "inselect" or "not inselect" # don't use param named because orm will add other param (test_active, ...) return [('id', op, (req, (user.id, user.karma, user.id, user.karma, user.id)))] @api.one @api.depends('content') def _get_plain_content(self): self.plain_content = tools.html2plaintext(self.content)[0:500] if self.content else False @api.one @api.depends('vote_count', 'forum_id.relevancy_post_vote', 'forum_id.relevancy_time_decay') def _compute_relevancy(self): if self.create_date: days = (datetime.today() - datetime.strptime(self.create_date, tools.DEFAULT_SERVER_DATETIME_FORMAT)).days self.relevancy = math.copysign(1, self.vote_count) * (abs(self.vote_count - 1) ** self.forum_id.relevancy_post_vote / (days + 2) ** self.forum_id.relevancy_time_decay) else: self.relevancy = 0 @api.multi def _get_user_vote(self): votes = self.env['forum.post.vote'].search_read([('post_id', 'in', self._ids), ('user_id', '=', self._uid)], ['vote', 'post_id']) mapped_vote = dict([(v['post_id'][0], v['vote']) for v in votes]) for vote in self: vote.user_vote = mapped_vote.get(vote.id, 0) @api.multi @api.depends('vote_ids.vote') def _get_vote_count(self): read_group_res = self.env['forum.post.vote'].read_group([('post_id', 'in', self._ids)], ['post_id', 'vote'], ['post_id', 'vote'], lazy=False) result = dict.fromkeys(self._ids, 0) for data in read_group_res: result[data['post_id'][0]] += data['__count'] * int(data['vote']) for post in self: post.vote_count = result[post.id] @api.one def _get_user_favourite(self): self.user_favourite = self._uid in self.favourite_ids.ids @api.one @api.depends('favourite_ids') def _get_favorite_count(self): self.favourite_count = len(self.favourite_ids) @api.one @api.depends('create_uid', 'parent_id') def _is_self_reply(self): self.self_reply = self.parent_id.create_uid.id == self._uid @api.one @api.depends('child_ids.create_uid', 'website_message_ids') def _get_child_count(self): def process(node): total = len(node.website_message_ids) + len(node.child_ids) for child in node.child_ids: total += process(child) return total self.child_count = process(self) @api.one def _get_uid_has_answered(self): self.uid_has_answered = any(answer.create_uid.id == self._uid for answer in self.child_ids) @api.one @api.depends('child_ids.is_correct') def _get_has_validated_answer(self): self.has_validated_answer = any(answer.is_correct for answer in self.child_ids) @api.multi def _get_post_karma_rights(self): user = self.env.user is_admin = user.id == SUPERUSER_ID # sudoed recordset instead of individual posts so values can be # prefetched in bulk for post, post_sudo in pycompat.izip(self, self.sudo()): is_creator = post.create_uid == user post.karma_accept = post.forum_id.karma_answer_accept_own if post.parent_id.create_uid == user else post.forum_id.karma_answer_accept_all post.karma_edit = post.forum_id.karma_edit_own if is_creator else post.forum_id.karma_edit_all post.karma_close = post.forum_id.karma_close_own if is_creator else post.forum_id.karma_close_all post.karma_unlink = post.forum_id.karma_unlink_own if is_creator else post.forum_id.karma_unlink_all post.karma_comment = post.forum_id.karma_comment_own if is_creator else post.forum_id.karma_comment_all post.karma_comment_convert = post.forum_id.karma_comment_convert_own if is_creator else post.forum_id.karma_comment_convert_all post.can_ask = is_admin or user.karma >= post.forum_id.karma_ask post.can_answer = is_admin or user.karma >= post.forum_id.karma_answer post.can_accept = is_admin or user.karma >= post.karma_accept post.can_edit = is_admin or user.karma >= post.karma_edit post.can_close = is_admin or user.karma >= post.karma_close post.can_unlink = is_admin or user.karma >= post.karma_unlink post.can_upvote = is_admin or user.karma >= post.forum_id.karma_upvote post.can_downvote = is_admin or user.karma >= post.forum_id.karma_downvote post.can_comment = is_admin or user.karma >= post.karma_comment post.can_comment_convert = is_admin or user.karma >= post.karma_comment_convert post.can_view = is_admin or user.karma >= post.karma_close or (post_sudo.create_uid.karma > 0 and (post_sudo.active or post_sudo.create_uid == user)) post.can_display_biography = is_admin or post_sudo.create_uid.karma >= post.forum_id.karma_user_bio post.can_post = is_admin or user.karma >= post.forum_id.karma_post post.can_flag = is_admin or user.karma >= post.forum_id.karma_flag post.can_moderate = is_admin or user.karma >= post.forum_id.karma_moderate @api.one @api.constrains('post_type', 'forum_id') def _check_post_type(self): if (self.post_type == 'question' and not self.forum_id.allow_question) \ or (self.post_type == 'discussion' and not self.forum_id.allow_discussion) \ or (self.post_type == 'link' and not self.forum_id.allow_link): raise ValidationError(_('This forum does not allow %s') % self.post_type) def _update_content(self, content, forum_id): forum = self.env['forum.forum'].browse(forum_id) if content and self.env.user.karma < forum.karma_dofollow: for match in re.findall(r'<a\s.*href=".*?">', content): match = re.escape(match) # replace parenthesis or special char in regex content = re.sub(match, match[:3] + 'rel="nofollow" ' + match[3:], content) if self.env.user.karma <= forum.karma_editor: filter_regexp = r'(<img.*?>)|(<a[^>]*?href[^>]*?>)|(<[a-z|A-Z]+[^>]*style\s*=\s*[\'"][^\'"]*\s*background[^:]*:[^url;]*url)' content_match = re.search(filter_regexp, content, re.I) if content_match: raise KarmaError('User karma not sufficient to post an image or link.') return content @api.model def create(self, vals): if 'content' in vals and vals.get('forum_id'): vals['content'] = self._update_content(vals['content'], vals['forum_id']) post = super(Post, self.with_context(mail_create_nolog=True)).create(vals) # deleted or closed questions if post.parent_id and (post.parent_id.state == 'close' or post.parent_id.active is False): raise UserError(_('Posting answer on a [Deleted] or [Closed] question is not possible')) # karma-based access if not post.parent_id and not post.can_ask: raise KarmaError('Not enough karma to create a new question') elif post.parent_id and not post.can_answer: raise KarmaError('Not enough karma to answer to a question') if not post.parent_id and not post.can_post: post.sudo().state = 'pending' # add karma for posting new questions if not post.parent_id and post.state == 'active': self.env.user.sudo().add_karma(post.forum_id.karma_gen_question_new) post.post_notification() return post @api.model def check_mail_message_access(self, res_ids, operation, model_name=None): if operation in ('write', 'unlink') and (not model_name or model_name == 'forum.post'): # Make sure only author or moderator can edit/delete messages if any(not post.can_edit for post in self.browse(res_ids)): raise KarmaError('Not enough karma to edit a post.') return super(Post, self).check_mail_message_access(res_ids, operation, model_name=model_name) @api.multi @api.depends('name', 'post_type') def name_get(self): result = [] for post in self: if post.post_type == 'discussion' and post.parent_id and not post.name: result.append((post.id, '%s (%s)' % (post.parent_id.name, post.id))) else: result.append((post.id, '%s' % (post.name))) return result @api.multi def write(self, vals): trusted_keys = ['active', 'is_correct', 'tag_ids'] # fields where security is checked manually if 'content' in vals: vals['content'] = self._update_content(vals['content'], self.forum_id.id) if 'state' in vals: if vals['state'] in ['active', 'close']: if any(not post.can_close for post in self): raise KarmaError('Not enough karma to close or reopen a post.') trusted_keys += ['state', 'closed_uid', 'closed_date', 'closed_reason_id'] elif vals['state'] == 'flagged': if any(not post.can_flag for post in self): raise KarmaError('Not enough karma to flag a post.') trusted_keys += ['state', 'flag_user_id'] if 'active' in vals: if any(not post.can_unlink for post in self): raise KarmaError('Not enough karma to delete or reactivate a post') if 'is_correct' in vals: if any(not post.can_accept for post in self): raise KarmaError('Not enough karma to accept or refuse an answer') # update karma except for self-acceptance mult = 1 if vals['is_correct'] else -1 for post in self: if vals['is_correct'] != post.is_correct and post.create_uid.id != self._uid: post.create_uid.sudo().add_karma(post.forum_id.karma_gen_answer_accepted * mult) self.env.user.sudo().add_karma(post.forum_id.karma_gen_answer_accept * mult) if 'tag_ids' in vals: tag_ids = set(tag.get('id') for tag in self.resolve_2many_commands('tag_ids', vals['tag_ids'])) if any(set(post.tag_ids) != tag_ids for post in self) and any(self.env.user.karma < post.forum_id.karma_edit_retag for post in self): raise KarmaError(_('Not enough karma to retag.')) if any(key not in trusted_keys for key in vals) and any(not post.can_edit for post in self): raise KarmaError('Not enough karma to edit a post.') res = super(Post, self).write(vals) # if post content modify, notify followers if 'content' in vals or 'name' in vals: for post in self: if post.parent_id: body, subtype = _('Answer Edited'), 'website_forum.mt_answer_edit' obj_id = post.parent_id else: body, subtype = _('Question Edited'), 'website_forum.mt_question_edit' obj_id = post obj_id.message_post(body=body, subtype=subtype) if 'active' in vals: answers = self.env['forum.post'].with_context(active_test=False).search([('parent_id', 'in', self.ids)]) if answers: answers.write({'active': vals['active']}) return res @api.multi def post_notification(self): for post in self: tag_partners = post.tag_ids.mapped('message_partner_ids') tag_channels = post.tag_ids.mapped('message_channel_ids') if post.state == 'active' and post.parent_id: post.parent_id.message_post_with_view( 'website_forum.forum_post_template_new_answer', subject=_('Re: %s') % post.parent_id.name, partner_ids=[(4, p.id) for p in tag_partners], channel_ids=[(4, c.id) for c in tag_channels], subtype_id=self.env['ir.model.data'].xmlid_to_res_id('website_forum.mt_answer_new')) elif post.state == 'active' and not post.parent_id: post.message_post_with_view( 'website_forum.forum_post_template_new_question', subject=post.name, partner_ids=[(4, p.id) for p in tag_partners], channel_ids=[(4, c.id) for c in tag_channels], subtype_id=self.env['ir.model.data'].xmlid_to_res_id('website_forum.mt_question_new')) elif post.state == 'pending' and not post.parent_id: # TDE FIXME: in master, you should probably use a subtype; # however here we remove subtype but set partner_ids partners = post.sudo().message_partner_ids | tag_partners partners = partners.filtered(lambda partner: partner.user_ids and any(user.karma >= post.forum_id.karma_moderate for user in partner.user_ids)) post.message_post_with_view( 'website_forum.forum_post_template_validation', subject=post.name, partner_ids=partners.ids, subtype_id=self.env['ir.model.data'].xmlid_to_res_id('mail.mt_note')) return True @api.multi def reopen(self): if any(post.parent_id or post.state != 'close' for post in self): return False reason_offensive = self.env.ref('website_forum.reason_7') reason_spam = self.env.ref('website_forum.reason_8') for post in self: if post.closed_reason_id in (reason_offensive, reason_spam): _logger.info('Upvoting user <%s>, reopening spam/offensive question', post.create_uid) karma = post.forum_id.karma_gen_answer_flagged if post.closed_reason_id == reason_spam: # If first post, increase the karma to add count_post = post.search_count([('parent_id', '=', False), ('forum_id', '=', post.forum_id.id), ('create_uid', '=', post.create_uid.id)]) if count_post == 1: karma *= 10 post.create_uid.sudo().add_karma(karma * -1) self.sudo().write({'state': 'active'}) @api.multi def close(self, reason_id): if any(post.parent_id for post in self): return False reason_offensive = self.env.ref('website_forum.reason_7').id reason_spam = self.env.ref('website_forum.reason_8').id if reason_id in (reason_offensive, reason_spam): for post in self: _logger.info('Downvoting user <%s> for posting spam/offensive contents', post.create_uid) karma = post.forum_id.karma_gen_answer_flagged if reason_id == reason_spam: # If first post, increase the karma to remove count_post = post.search_count([('parent_id', '=', False), ('forum_id', '=', post.forum_id.id), ('create_uid', '=', post.create_uid.id)]) if count_post == 1: karma *= 10 post.create_uid.sudo().add_karma(karma) self.write({ 'state': 'close', 'closed_uid': self._uid, 'closed_date': datetime.today().strftime(tools.DEFAULT_SERVER_DATETIME_FORMAT), 'closed_reason_id': reason_id, }) return True @api.one def validate(self): if not self.can_moderate: raise KarmaError('Not enough karma to validate a post') # if state == pending, no karma previously added for the new question if self.state == 'pending': self.create_uid.sudo().add_karma(self.forum_id.karma_gen_question_new) self.write({ 'state': 'active', 'active': True, 'moderator_id': self.env.user.id, }) self.post_notification() return True @api.one def refuse(self): if not self.can_moderate: raise KarmaError('Not enough karma to refuse a post') self.moderator_id = self.env.user return True @api.one def flag(self): if not self.can_flag: raise KarmaError('Not enough karma to flag a post') if(self.state == 'flagged'): return {'error': 'post_already_flagged'} elif(self.state == 'active'): self.write({ 'state': 'flagged', 'flag_user_id': self.env.user.id, }) return self.can_moderate and {'success': 'post_flagged_moderator'} or {'success': 'post_flagged_non_moderator'} else: return {'error': 'post_non_flaggable'} @api.one def mark_as_offensive(self, reason_id): if not self.can_moderate: raise KarmaError('Not enough karma to mark a post as offensive') # remove some karma _logger.info('Downvoting user <%s> for posting spam/offensive contents', self.create_uid) self.create_uid.sudo().add_karma(self.forum_id.karma_gen_answer_flagged) self.write({ 'state': 'offensive', 'moderator_id': self.env.user.id, 'closed_date': datetime.today().strftime(tools.DEFAULT_SERVER_DATETIME_FORMAT), 'closed_reason_id': reason_id, 'active': False, }) return True @api.multi def unlink(self): if any(not post.can_unlink for post in self): raise KarmaError('Not enough karma to unlink a post') # if unlinking an answer with accepted answer: remove provided karma for post in self: if post.is_correct: post.create_uid.sudo().add_karma(post.forum_id.karma_gen_answer_accepted * -1) self.env.user.sudo().add_karma(post.forum_id.karma_gen_answer_accepted * -1) return super(Post, self).unlink() @api.multi def bump(self): """ Bump a question: trigger a write_date by writing on a dummy bump_date field. One cannot bump a question more than once every 10 days. """ self.ensure_one() if self.forum_id.allow_bump and not self.child_ids and (datetime.today() - datetime.strptime(self.write_date, tools.DEFAULT_SERVER_DATETIME_FORMAT)).days > 9: # write through super to bypass karma; sudo to allow public user to bump any post return self.sudo().write({'bump_date': fields.Datetime.now()}) return False @api.multi def vote(self, upvote=True): Vote = self.env['forum.post.vote'] vote_ids = Vote.search([('post_id', 'in', self._ids), ('user_id', '=', self._uid)]) new_vote = '1' if upvote else '-1' voted_forum_ids = set() if vote_ids: for vote in vote_ids: if upvote: new_vote = '0' if vote.vote == '-1' else '1' else: new_vote = '0' if vote.vote == '1' else '-1' vote.vote = new_vote voted_forum_ids.add(vote.post_id.id) for post_id in set(self._ids) - voted_forum_ids: for post_id in self._ids: Vote.create({'post_id': post_id, 'vote': new_vote}) return {'vote_count': self.vote_count, 'user_vote': new_vote} @api.multi def convert_answer_to_comment(self): """ Tools to convert an answer (forum.post) to a comment (mail.message). The original post is unlinked and a new comment is posted on the question using the post create_uid as the comment's author. """ self.ensure_one() if not self.parent_id: return self.env['mail.message'] # karma-based action check: use the post field that computed own/all value if not self.can_comment_convert: raise KarmaError('Not enough karma to convert an answer to a comment') # post the message question = self.parent_id values = { 'author_id': self.sudo().create_uid.partner_id.id, # use sudo here because of access to res.users model 'body': tools.html_sanitize(self.content, sanitize_attributes=True, strip_style=True, strip_classes=True), 'message_type': 'comment', 'subtype': 'mail.mt_comment', 'date': self.create_date, } new_message = question.with_context(mail_create_nosubscribe=True).message_post(**values) # unlink the original answer, using SUPERUSER_ID to avoid karma issues self.sudo().unlink() return new_message @api.model def convert_comment_to_answer(self, message_id, default=None): """ Tool to convert a comment (mail.message) into an answer (forum.post). The original comment is unlinked and a new answer from the comment's author is created. Nothing is done if the comment's author already answered the question. """ comment = self.env['mail.message'].sudo().browse(message_id) post = self.browse(comment.res_id) if not comment.author_id or not comment.author_id.user_ids: # only comment posted by users can be converted return False # karma-based action check: must check the message's author to know if own / all karma_convert = comment.author_id.id == self.env.user.partner_id.id and post.forum_id.karma_comment_convert_own or post.forum_id.karma_comment_convert_all can_convert = self.env.user.karma >= karma_convert if not can_convert: raise KarmaError('Not enough karma to convert a comment to an answer') # check the message's author has not already an answer question = post.parent_id if post.parent_id else post post_create_uid = comment.author_id.user_ids[0] if any(answer.create_uid.id == post_create_uid.id for answer in question.child_ids): return False # create the new post post_values = { 'forum_id': question.forum_id.id, 'content': comment.body, 'parent_id': question.id, } # done with the author user to have create_uid correctly set new_post = self.sudo(post_create_uid.id).create(post_values) # delete comment comment.unlink() return new_post @api.one def unlink_comment(self, message_id): user = self.env.user comment = self.env['mail.message'].sudo().browse(message_id) if not comment.model == 'forum.post' or not comment.res_id == self.id: return False # karma-based action check: must check the message's author to know if own or all karma_unlink = comment.author_id.id == user.partner_id.id and self.forum_id.karma_comment_unlink_own or self.forum_id.karma_comment_unlink_all can_unlink = user.karma >= karma_unlink if not can_unlink: raise KarmaError('Not enough karma to unlink a comment') return comment.unlink() @api.multi def set_viewed(self): self._cr.execute("""UPDATE forum_post SET views = views+1 WHERE id IN %s""", (self._ids,)) return True @api.multi def get_access_action(self, access_uid=None): """ Instead of the classic form view, redirect to the post on the website directly """ self.ensure_one() return { 'type': 'ir.actions.act_url', 'url': '/forum/%s/question/%s' % (self.forum_id.id, self.id), 'target': 'self', 'target_type': 'public', 'res_id': self.id, } @api.multi def _notification_recipients(self, message, groups): groups = super(Post, self)._notification_recipients(message, groups) for group_name, group_method, group_data in groups: group_data['has_button_access'] = True return groups @api.multi @api.returns('self', lambda value: value.id) def message_post(self, message_type='notification', subtype=None, **kwargs): question_followers = self.env['res.partner'] if self.ids and message_type == 'comment': # user comments have a restriction on karma # add followers of comments on the parent post if self.parent_id: partner_ids = kwargs.get('partner_ids', []) comment_subtype = self.sudo().env.ref('mail.mt_comment') question_followers = self.env['mail.followers'].sudo().search([ ('res_model', '=', self._name), ('res_id', '=', self.parent_id.id), ('partner_id', '!=', False), ]).filtered(lambda fol: comment_subtype in fol.subtype_ids).mapped('partner_id') partner_ids += [(4, partner.id) for partner in question_followers] kwargs['partner_ids'] = partner_ids self.ensure_one() if not self.can_comment: raise KarmaError('Not enough karma to comment') if not kwargs.get('record_name') and self.parent_id: kwargs['record_name'] = self.parent_id.name return super(Post, self).message_post(message_type=message_type, subtype=subtype, **kwargs) @api.multi def message_get_message_notify_values(self, message, message_values): """ Override to avoid keeping all notified recipients of a comment. We avoid tracking needaction on post comments. Only emails should be sufficient. """ if message.message_type == 'comment': return { 'needaction_partner_ids': [], 'partner_ids': [], } return {}
class FinancialYearOpeningWizard(models.TransientModel): _name = 'account.financial.year.op' company_id = fields.Many2one(comodel_name='res.company', required=True) opening_move_posted = fields.Boolean( string='Opening Move Posted', compute='_compute_opening_move_posted') opening_date = fields.Date( string='Opening Date', required=True, related='company_id.account_opening_date', help= "Date from which the accounting is managed in actpy. It is the date of the opening entry." ) fiscalyear_last_day = fields.Integer( related="company_id.fiscalyear_last_day", required=True, help= "The last day of the month will be taken if the chosen day doesn't exist." ) fiscalyear_last_month = fields.Selection( selection=[(1, 'January'), (2, 'February'), (3, 'March'), (4, 'April'), (5, 'May'), (6, 'June'), (7, 'July'), (8, 'August'), (9, 'September'), (10, 'October'), (11, 'November'), (12, 'December')], related="company_id.fiscalyear_last_month", required=True, help= "The last day of the month will be taken if the chosen day doesn't exist." ) account_setup_fy_data_done = fields.Boolean( string='Financial year setup marked as done', compute="_compute_setup_marked_done") @api.depends('company_id.account_setup_fy_data_done') def _compute_setup_marked_done(self): for record in self: record.account_setup_fy_data_done = record.company_id.account_setup_fy_data_done @api.depends('company_id.account_opening_move_id') def _compute_opening_move_posted(self): for record in self: record.opening_move_posted = record.company_id.opening_move_posted( ) def mark_as_done(self): """ Forces fiscal year setup state to 'done'.""" self.company_id.account_setup_fy_data_done = True def unmark_as_done(self): """ Forces fiscal year setup state to 'undone'.""" self.company_id.account_setup_fy_data_done = False @api.multi def write(self, vals): if 'fiscalyear_last_day' in vals or 'fiscalyear_last_month' in vals: for wizard in self: company = wizard.company_id vals[ 'fiscalyear_last_day'] = company._verify_fiscalyear_last_day( company.id, vals.get('fiscalyear_last_day'), vals.get('fiscalyear_last_month')) return super(FinancialYearOpeningWizard, self).write(vals)
class CompanyLDAP(models.Model): _name = 'res.company.ldap' _order = 'sequence' _rec_name = 'ldap_server' sequence = fields.Integer(default=10) company = fields.Many2one('res.company', string='Company', required=True, ondelete='cascade') ldap_server = fields.Char(string='LDAP Server address', required=True, default='127.0.0.1') ldap_server_port = fields.Integer(string='LDAP Server port', required=True, default=389) ldap_binddn = fields.Char( 'LDAP binddn', help= "The user account on the LDAP server that is used to query the directory. " "Leave empty to connect anonymously.") ldap_password = fields.Char( string='LDAP password', help= "The password of the user account on the LDAP server that is used to query the directory." ) ldap_filter = fields.Char(string='LDAP filter', required=True) ldap_base = fields.Char(string='LDAP base', required=True) user = fields.Many2one('res.users', string='Template User', help="User to copy when creating new users") create_user = fields.Boolean( default=True, help= "Automatically create local user accounts for new users authenticating via LDAP" ) ldap_tls = fields.Boolean( string='Use TLS', help= "Request secure TLS/SSL encryption when connecting to the LDAP server. " "This option requires a server with STARTTLS enabled, " "otherwise all authentication attempts will fail.") @api.multi def get_ldap_dicts(self): """ Retrieve res_company_ldap resources from the database in dictionary format. :return: ldap configurations :rtype: list of dictionaries """ ldaps = self.sudo().search([('ldap_server', '!=', False)], order='sequence') res = ldaps.read([ 'id', 'company', 'ldap_server', 'ldap_server_port', 'ldap_binddn', 'ldap_password', 'ldap_filter', 'ldap_base', 'user', 'create_user', 'ldap_tls' ]) return res def connect(self, conf): """ Connect to an LDAP server specified by an ldap configuration dictionary. :param dict conf: LDAP configuration :return: an LDAP object """ uri = 'ldap://%s:%d' % (conf['ldap_server'], conf['ldap_server_port']) connection = ldap.initialize(uri) if conf['ldap_tls']: connection.start_tls_s() return connection def authenticate(self, conf, login, password): """ Authenticate a user against the specified LDAP server. In order to prevent an unintended 'unauthenticated authentication', which is an anonymous bind with a valid dn and a blank password, check for empty passwords explicitely (:rfc:`4513#section-6.3.1`) :param dict conf: LDAP configuration :param login: username :param password: Password for the LDAP user :return: LDAP entry of authenticated user or False :rtype: dictionary of attributes """ if not password: return False entry = False try: filter = filter_format(conf['ldap_filter'], (login, )) except TypeError: _logger.warning( 'Could not format LDAP filter. Your filter should contain one \'%s\'.' ) return False try: results = self.query(conf, tools.ustr(filter)) # Get rid of (None, attrs) for searchResultReference replies results = [i for i in results if i[0]] if len(results) == 1: dn = results[0][0] conn = self.connect(conf) conn.simple_bind_s(dn, to_native(password)) conn.unbind() entry = results[0] except ldap.INVALID_CREDENTIALS: return False except ldap.LDAPError as e: _logger.error('An LDAP exception occurred: %s', e) return entry def query(self, conf, filter, retrieve_attributes=None): """ Query an LDAP server with the filter argument and scope subtree. Allow for all authentication methods of the simple authentication method: - authenticated bind (non-empty binddn + valid password) - anonymous bind (empty binddn + empty password) - unauthenticated authentication (non-empty binddn + empty password) .. seealso:: :rfc:`4513#section-5.1` - LDAP: Simple Authentication Method. :param dict conf: LDAP configuration :param filter: valid LDAP filter :param list retrieve_attributes: LDAP attributes to be retrieved. \ If not specified, return all attributes. :return: ldap entries :rtype: list of tuples (dn, attrs) """ results = [] try: conn = self.connect(conf) ldap_password = conf['ldap_password'] or '' ldap_binddn = conf['ldap_binddn'] or '' conn.simple_bind_s(to_native(ldap_binddn), to_native(ldap_password)) results = conn.search_st(to_native(conf['ldap_base']), ldap.SCOPE_SUBTREE, filter, retrieve_attributes, timeout=60) conn.unbind() except ldap.INVALID_CREDENTIALS: _logger.error('LDAP bind failed.') except ldap.LDAPError as e: _logger.error('An LDAP exception occurred: %s', e) return results def map_ldap_attributes(self, conf, login, ldap_entry): """ Compose values for a new resource of model res_users, based upon the retrieved ldap entry and the LDAP settings. :param dict conf: LDAP configuration :param login: the new user's login :param tuple ldap_entry: single LDAP result (dn, attrs) :return: parameters for a new resource of model res_users :rtype: dict """ return { 'name': ldap_entry[1]['cn'][0], 'login': login, 'company_id': conf['company'][0] } @api.model def get_or_create_user(self, conf, login, ldap_entry): """ Retrieve an active resource of model res_users with the specified login. Create the user if it is not initially found. :param dict conf: LDAP configuration :param login: the user's login :param tuple ldap_entry: single LDAP result (dn, attrs) :return: res_users id :rtype: int """ user_id = False login = tools.ustr(login.lower().strip()) self.env.cr.execute( "SELECT id, active FROM res_users WHERE lower(login)=%s", (login, )) res = self.env.cr.fetchone() if res: if res[1]: user_id = res[0] elif conf['create_user']: _logger.debug("Creating new actpy user \"%s\" from LDAP" % login) values = self.map_ldap_attributes(conf, login, ldap_entry) SudoUser = self.env['res.users'].sudo() if conf['user']: values['active'] = True user_id = SudoUser.browse( conf['user'][0]).copy(default=values).id else: user_id = SudoUser.create(values).id return user_id
class SaleOrder(models.Model): _inherit = 'sale.order' carrier_id = fields.Many2one( 'delivery.carrier', string="Delivery Method", help= "Fill this field if you plan to invoice the shipping based on picking." ) delivery_price = fields.Float(string='Estimated Delivery Price', readonly=True, copy=False) delivery_message = fields.Char(readonly=True, copy=False) delivery_rating_success = fields.Boolean(copy=False) invoice_shipping_on_delivery = fields.Boolean( string="Invoice Shipping on Delivery", copy=False) def _compute_amount_total_without_delivery(self): self.ensure_one() delivery_cost = sum( [l.price_total for l in self.order_line if l.is_delivery]) return self.amount_total - delivery_cost def get_delivery_price(self): for order in self.filtered(lambda o: o.state in ('draft', 'sent') and len(o.order_line) > 0): # We do not want to recompute the shipping price of an already validated/done SO # or on an SO that has no lines yet order.delivery_rating_success = False res = order.carrier_id.rate_shipment(order) if res['success']: order.delivery_rating_success = True order.delivery_price = res['price'] order.delivery_message = res['warning_message'] else: order.delivery_rating_success = False order.delivery_price = 0.0 order.delivery_message = res['error_message'] @api.onchange('carrier_id') def onchange_carrier_id(self): if self.state in ('draft', 'sent'): self.delivery_price = 0.0 self.delivery_rating_success = False self.delivery_message = False @api.onchange('partner_id') def onchange_partner_id_carrier_id(self): if self.partner_id: self.carrier_id = self.partner_id.property_delivery_carrier_id # TODO onchange sol, clean delivery price @api.multi def action_confirm(self): res = super(SaleOrder, self).action_confirm() for so in self: so.invoice_shipping_on_delivery = all( [not line.is_delivery for line in so.order_line]) return res @api.multi def _remove_delivery_line(self): self.env['sale.order.line'].search([('order_id', 'in', self.ids), ('is_delivery', '=', True) ]).unlink() @api.multi def set_delivery_line(self): # Remove delivery products from the sales order self._remove_delivery_line() for order in self: if order.state not in ('draft', 'sent'): raise UserError( _('You can add delivery price only on unconfirmed quotations.' )) elif not order.carrier_id: raise UserError(_('No carrier set for this order.')) elif not order.delivery_rating_success: raise UserError( _('Please use "Check price" in order to compute a shipping price for this quotation.' )) else: price_unit = order.carrier_id.rate_shipment(order)['price'] # TODO check whether it is safe to use delivery_price here order._create_delivery_line(order.carrier_id, price_unit) return True def _create_delivery_line(self, carrier, price_unit): SaleOrderLine = self.env['sale.order.line'] # Apply fiscal position taxes = carrier.product_id.taxes_id.filtered( lambda t: t.company_id.id == self.company_id.id) taxes_ids = taxes.ids if self.partner_id and self.fiscal_position_id: taxes_ids = self.fiscal_position_id.map_tax( taxes, carrier.product_id, self.partner_id).ids # Create the sales order line values = { 'order_id': self.id, 'name': carrier.name, 'product_uom_qty': 1, 'product_uom': carrier.product_id.uom_id.id, 'product_id': carrier.product_id.id, 'price_unit': price_unit, 'tax_id': [(6, 0, taxes_ids)], 'is_delivery': True, } if self.order_line: values['sequence'] = self.order_line[-1].sequence + 1 sol = SaleOrderLine.sudo().create(values) return sol