class Lead2OpportunityMassConvert(models.TransientModel): _name = 'crm.lead2opportunity.partner.mass' _description = 'Mass Lead To Opportunity Partner' _inherit = 'crm.lead2opportunity.partner' @api.model def default_get(self, fields): res = super(Lead2OpportunityMassConvert, self).default_get(fields) if 'partner_id' in fields: # avoid forcing the partner of the first lead as default res['partner_id'] = False if 'action' in fields: res['action'] = 'each_exist_or_create' if 'name' in fields: res['name'] = 'convert' if 'opportunity_ids' in fields: res['opportunity_ids'] = False return res user_ids = fields.Many2many('res.users', string='Salesmen') team_id = fields.Many2one('crm.team', 'Sales Channel', index=True, oldname='section_id') deduplicate = fields.Boolean( 'Apply deduplication', default=True, help='Merge with existing leads/opportunities of each partner') action = fields.Selection( [('each_exist_or_create', 'Use existing partner or create'), ('nothing', 'Do not link to a customer')], 'Related Customer', required=True) force_assignation = fields.Boolean( 'Force assignation', help= 'If unchecked, this will leave the salesman of duplicated opportunities' ) @api.onchange('action') def _onchange_action(self): if self.action != 'exist': self.partner_id = False @api.onchange('deduplicate') def _onchange_deduplicate(self): active_leads = self.env['crm.lead'].browse(self._context['active_ids']) partner_ids = [ (lead.partner_id.id, lead.partner_id and lead.partner_id.email or lead.email_from) for lead in active_leads ] partners_duplicated_leads = {} for partner_id, email in partner_ids: duplicated_leads = self._get_duplicated_leads(partner_id, email) if len(duplicated_leads) > 1: partners_duplicated_leads.setdefault( (partner_id, email), []).extend(duplicated_leads) leads_with_duplicates = [] for lead in active_leads: lead_tuple = (lead.partner_id.id, lead.partner_id.email if lead.partner_id else lead.email_from) if len(partners_duplicated_leads.get(lead_tuple, [])) > 1: leads_with_duplicates.append(lead.id) self.opportunity_ids = self.env['crm.lead'].browse( leads_with_duplicates) @api.multi def _convert_opportunity(self, vals): """ When "massively" (more than one at a time) converting leads to opportunities, check the salesteam_id and salesmen_ids and update the values before calling super. """ self.ensure_one() salesteam_id = self.team_id.id if self.team_id else False salesmen_ids = [] if self.user_ids: salesmen_ids = self.user_ids.ids vals.update({'user_ids': salesmen_ids, 'team_id': salesteam_id}) return super(Lead2OpportunityMassConvert, self)._convert_opportunity(vals) @api.multi def mass_convert(self): self.ensure_one() if self.name == 'convert' and self.deduplicate: merged_lead_ids = set() remaining_lead_ids = set() lead_selected = self._context.get('active_ids', []) for lead_id in lead_selected: if lead_id not in merged_lead_ids: lead = self.env['crm.lead'].browse(lead_id) duplicated_leads = self._get_duplicated_leads( lead.partner_id.id, lead.partner_id.email if lead.partner_id else lead.email_from) if len(duplicated_leads) > 1: lead = duplicated_leads.merge_opportunity() merged_lead_ids.update(duplicated_leads.ids) remaining_lead_ids.add(lead.id) active_ids = set(self._context.get('active_ids', {})) active_ids = (active_ids - merged_lead_ids) | remaining_lead_ids self = self.with_context(active_ids=list( active_ids)) # only update active_ids when there are set no_force_assignation = self._context.get('no_force_assignation', not self.force_assignation) return self.with_context( no_force_assignation=no_force_assignation).action_apply()
class ProductTemplate(models.Model): _inherit = [ "product.template", "website.seo.metadata", 'website.published.mixin', 'rating.mixin' ] _order = 'website_published desc, website_sequence desc, name' _name = 'product.template' _mail_post_access = '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 website_description = fields.Html('Description for the website', sanitize_attributes=False, translate=html_translate) alternative_product_ids = fields.Many2many( 'product.template', 'product_alternative_rel', 'src_id', 'dest_id', string='Alternative Products', help='Suggest more expensive alternatives to ' 'your customers (upsell strategy). Those products show up on the product page.' ) accessory_product_ids = fields.Many2many( 'product.product', 'product_accessory_rel', 'src_id', 'dest_id', string='Accessory Products', help='Accessories show up when the customer reviews the ' 'cart before paying (cross-sell strategy, e.g. for computers: mouse, keyboard, etc.). ' 'An algorithm figures out a list of accessories based on all the products added to cart.' ) website_size_x = fields.Integer('Size X', default=1) website_size_y = fields.Integer('Size Y', default=1) website_sequence = fields.Integer( 'Website Sequence', help="Determine the display order in the Website E-commerce", default=lambda self: self._default_website_sequence()) public_categ_ids = fields.Many2many( 'product.public.category', string='Website Product Category', help= "Categories can be published on the Shop page (online catalog grid) to help " "customers find all the items within a category. To publish them, go to the Shop page, " "hit Customize and turn *Product Categories* on. A product can belong to several categories." ) product_image_ids = fields.One2many('product.image', 'product_tmpl_id', string='Images') website_price = fields.Float('Website price', compute='_website_price', digits=dp.get_precision('Product Price')) website_public_price = fields.Float( 'Website public price', compute='_website_price', digits=dp.get_precision('Product Price')) website_price_difference = fields.Boolean('Website price difference', compute='_website_price') website_ids = fields.Many2many('website', 'website_prod_pub_rel', 'website_id', 'product_id', string='Websites', copy=False, default=_default_website, help='List of websites in which ' 'Product will published.') ribbon_id = fields.Many2one('product.ribbon', string="Product Ribbon") brand_id = fields.Many2one('product.brand', string="Product Brand") tag_ids = fields.Many2many('product.tags', string="Product Tags") def _website_price(self): # First filter out the ones that have no variant: # This makes sure that every template below has a corresponding product in the zipped result. self = self.filtered('product_variant_id') # use mapped who returns a recordset with only itself to prefetch (and don't prefetch every product_variant_ids) for template, product in pycompat.izip( self, self.mapped('product_variant_id')): template.website_price = product.website_price template.website_public_price = product.website_public_price template.website_price_difference = product.website_price_difference def _default_website_sequence(self): self._cr.execute("SELECT MIN(website_sequence) FROM %s" % self._table) min_sequence = self._cr.fetchone()[0] return min_sequence and min_sequence - 1 or 10 def set_sequence_top(self): self.website_sequence = self.sudo().search( [], order='website_sequence desc', limit=1).website_sequence + 1 def set_sequence_bottom(self): self.website_sequence = self.sudo().search( [], order='website_sequence', limit=1).website_sequence - 1 def set_sequence_up(self): previous_product_tmpl = self.sudo().search( [('website_sequence', '>', self.website_sequence), ('website_published', '=', self.website_published)], order='website_sequence', limit=1) if previous_product_tmpl: previous_product_tmpl.website_sequence, self.website_sequence = self.website_sequence, previous_product_tmpl.website_sequence else: self.set_sequence_top() def set_sequence_down(self): next_prodcut_tmpl = self.search( [('website_sequence', '<', self.website_sequence), ('website_published', '=', self.website_published)], order='website_sequence desc', limit=1) if next_prodcut_tmpl: next_prodcut_tmpl.website_sequence, self.website_sequence = self.website_sequence, next_prodcut_tmpl.website_sequence else: return self.set_sequence_bottom() @api.multi def _compute_website_url(self): super(ProductTemplate, self)._compute_website_url() for product in self: product.website_url = "/shop/product/%s" % (product.id, )
class PaymentTransaction(models.Model): """ Transaction Model. Each specific acquirer can extend the model by adding its own fields. Methods that can be added in an acquirer-specific implementation: - ``<name>_create``: method receiving values used when creating a new transaction and that returns a dictionary that will update those values. This method can be used to tweak some transaction values. Methods defined for convention, depending on your controllers: - ``<name>_form_feedback(self, data)``: method that handles the data coming from the acquirer after the transaction. It will generally receives data posted by the acquirer after the transaction. """ _name = 'payment.transaction' _description = 'Payment Transaction' _order = 'id desc' _rec_name = 'reference' @api.model def _lang_get(self): return self.env['res.lang'].get_installed() @api.model def _get_default_partner_country_id(self): return self.env['res.company']._company_default_get( 'payment.transaction').country_id.id create_date = fields.Datetime('Creation Date', readonly=True) date_validate = fields.Datetime('Validation Date') acquirer_id = fields.Many2one('payment.acquirer', 'Acquirer', required=True) provider = fields.Selection(string='Provider', related='acquirer_id.provider') type = fields.Selection([('validation', 'Validation of the bank card'), ('server2server', 'Server To Server'), ('form', 'Form'), ('form_save', 'Form with tokenization')], 'Type', default='form', required=True) state = fields.Selection([('draft', 'Draft'), ('pending', 'Pending'), ('authorized', 'Authorized'), ('done', 'Done'), ('refunding', 'Refunding'), ('refunded', 'Refunded'), ('error', 'Error'), ('cancel', 'Canceled')], 'Status', copy=False, default='draft', required=True, track_visibility='onchange') state_message = fields.Text( 'Message', help= 'Field used to store error and/or validation messages for information') # payment amount = fields.Float('Amount', digits=(16, 2), required=True, track_visibility='always', help='Amount') fees = fields.Float( 'Fees', digits=(16, 2), track_visibility='always', help='Fees amount; set by the system because depends on the acquirer') currency_id = fields.Many2one('res.currency', 'Currency', required=True) reference = fields.Char('Reference', default=lambda self: self.env['ir.sequence']. next_by_code('payment.transaction'), required=True, help='Internal reference of the TX') acquirer_reference = fields.Char( 'Acquirer Reference', help='Reference of the TX as stored in the acquirer database') # duplicate partner / transaction data to store the values at transaction time partner_id = fields.Many2one('res.partner', 'Customer', track_visibility='onchange') partner_name = fields.Char('Partner Name') partner_lang = fields.Selection(_lang_get, 'Language', default=lambda self: self.env.lang) partner_email = fields.Char('Email') partner_zip = fields.Char('Zip') partner_address = fields.Char('Address') partner_city = fields.Char('City') partner_country_id = fields.Many2one( 'res.country', 'Country', default=_get_default_partner_country_id, required=True) partner_phone = fields.Char('Phone') html_3ds = fields.Char('3D Secure HTML') callback_model_id = fields.Many2one('ir.model', 'Callback Document Model', groups="base.group_system") callback_res_id = fields.Integer('Callback Document ID', groups="base.group_system") callback_method = fields.Char('Callback Method', groups="base.group_system") callback_hash = fields.Char('Callback Hash', groups="base.group_system") payment_token_id = fields.Many2one( 'payment.token', 'Payment Token', domain="[('acquirer_id', '=', acquirer_id)]") @api.onchange('partner_id') def _onchange_partner_id(self): onchange_vals = self.on_change_partner_id(self.partner_id.id).get( 'value', {}) self.update(onchange_vals) @api.multi def on_change_partner_id(self, partner_id): partner = None if partner_id: partner = self.env['res.partner'].browse(partner_id) return { 'value': { 'partner_name': partner and partner.name or False, 'partner_lang': partner and partner.lang or 'en_US', 'partner_email': partner and partner.email or False, 'partner_zip': partner and partner.zip or False, 'partner_address': _partner_format_address(partner and partner.street or '', partner and partner.street2 or ''), 'partner_city': partner and partner.city or False, 'partner_country_id': partner and partner.country_id.id or self._get_default_partner_country_id(), 'partner_phone': partner and partner.phone or False, } } return {} @api.constrains('reference', 'state') def _check_reference(self): for transaction in self.filtered(lambda tx: tx.state not in ('cancel', 'error')): if self.search_count([('reference', '=', transaction.reference) ]) != 1: raise exceptions.ValidationError( _('The payment transaction reference must be unique!')) return True @api.constrains('state', 'acquirer_id') def _check_authorize_state(self): failed_tx = self.filtered( lambda tx: tx.state == 'authorized' and tx.acquirer_id. provider not in self.env['payment.acquirer']._get_feature_support( )['authorize']) if failed_tx: raise exceptions.ValidationError( _('The %s payment acquirers are not allowed to manual capture mode!' % failed_tx.mapped('acquirer_id.name'))) @api.model def create(self, values): if values.get('partner_id'): # @TDENOTE: not sure values.update( self.on_change_partner_id(values['partner_id'])['value']) # call custom create method if defined (i.e. ogone_create for ogone) if values.get('acquirer_id'): acquirer = self.env['payment.acquirer'].browse( values['acquirer_id']) # compute fees custom_method_name = '%s_compute_fees' % acquirer.provider if hasattr(acquirer, custom_method_name): fees = getattr(acquirer, custom_method_name)( values.get('amount', 0.0), values.get('currency_id'), values.get('partner_country_id')) values['fees'] = float_round(fees, 2) # custom create custom_method_name = '%s_create' % acquirer.provider if hasattr(acquirer, custom_method_name): values.update(getattr(self, custom_method_name)(values)) # Default value of reference is tx = super(PaymentTransaction, self).create(values) if not values.get('reference'): tx.write({'reference': str(tx.id)}) # Generate callback hash if it is configured on the tx; avoid generating unnecessary stuff # (limited sudo env for checking callback presence, must work for manual transactions too) tx_sudo = tx.sudo() if tx_sudo.callback_model_id and tx_sudo.callback_res_id and tx_sudo.callback_method: tx.write({'callback_hash': tx._generate_callback_hash()}) return tx @api.multi def write(self, values): if ('acquirer_id' in values or 'amount' in values) and 'fees' not in values: # The acquirer or the amount has changed, and the fees are not explicitly forced. Fees must be recomputed. acquirer = None if values.get('acquirer_id'): acquirer = self.env['payment.acquirer'].browse( values['acquirer_id']) for tx in self: vals = dict(values, fees=0.0) if not acquirer: acquirer = tx.acquirer_id custom_method_name = '%s_compute_fees' % acquirer.provider # TDE FIXME: shouldn't we use fee_implemented ? if hasattr(acquirer, custom_method_name): fees = getattr(acquirer, custom_method_name)( (values['amount'] if 'amount' in values else tx.amount) or 0.0, values.get('currency_id') or tx.currency_id.id, values.get('partner_country_id') or tx.partner_country_id.id) vals['fees'] = float_round(fees, 2) res = super(PaymentTransaction, tx).write(vals) return res return super(PaymentTransaction, self).write(values) @api.model def get_next_reference(self, reference): ref_suffix = 1 init_ref = reference while self.env['payment.transaction'].sudo().search_count([ ('reference', '=', reference) ]): reference = init_ref + 'x' + str(ref_suffix) ref_suffix += 1 return reference def _generate_callback_hash(self): self.ensure_one() secret = self.env['ir.config_parameter'].sudo().get_param( 'database.secret') token = '%s%s%s' % (self.callback_model_id.model, self.callback_res_id, self.sudo().callback_method) return hmac.new(secret.encode('utf-8'), token.encode('utf-8'), hashlib.sha256).hexdigest() # -------------------------------------------------- # FORM RELATED METHODS # -------------------------------------------------- @api.multi def render(self): values = { 'reference': self.reference, 'amount': self.amount, 'currency_id': self.currency_id.id, 'currency': self.currency_id, 'partner': self.partner_id, 'partner_name': self.partner_name, 'partner_lang': self.partner_lang, 'partner_email': self.partner_email, 'partner_zip': self.partner_zip, 'partner_address': self.partner_address, 'partner_city': self.partner_city, 'partner_country_id': self.partner_country_id.id, 'partner_country': self.partner_country_id, 'partner_phone': self.partner_phone, 'partner_state': None, } return self.acquirer_id.render(None, None, None, values=values) @api.model def form_feedback(self, data, acquirer_name): invalid_parameters, tx = None, None tx_find_method_name = '_%s_form_get_tx_from_data' % acquirer_name if hasattr(self, tx_find_method_name): tx = getattr(self, tx_find_method_name)(data) # TDE TODO: form_get_invalid_parameters from model to multi invalid_param_method_name = '_%s_form_get_invalid_parameters' % acquirer_name if hasattr(self, invalid_param_method_name): invalid_parameters = getattr(tx, invalid_param_method_name)(data) if invalid_parameters: _error_message = '%s: incorrect tx data:\n' % (acquirer_name) for item in invalid_parameters: _error_message += '\t%s: received %s instead of %s\n' % ( item[0], item[1], item[2]) _logger.error(_error_message) return False # TDE TODO: form_validate from model to multi feedback_method_name = '_%s_form_validate' % acquirer_name if hasattr(self, feedback_method_name): return getattr(tx, feedback_method_name)(data) return True @api.multi def _post_process_after_done(self, **kwargs): return True # -------------------------------------------------- # SERVER2SERVER RELATED METHODS # -------------------------------------------------- @api.multi def s2s_do_transaction(self, **kwargs): custom_method_name = '%s_s2s_do_transaction' % self.acquirer_id.provider if hasattr(self, custom_method_name): return getattr(self, custom_method_name)(**kwargs) @api.multi def s2s_do_refund(self, **kwargs): custom_method_name = '%s_s2s_do_refund' % self.acquirer_id.provider if hasattr(self, custom_method_name): return getattr(self, custom_method_name)(**kwargs) @api.multi def s2s_capture_transaction(self, **kwargs): custom_method_name = '%s_s2s_capture_transaction' % self.acquirer_id.provider if hasattr(self, custom_method_name): return getattr(self, custom_method_name)(**kwargs) @api.multi def s2s_void_transaction(self, **kwargs): custom_method_name = '%s_s2s_void_transaction' % self.acquirer_id.provider if hasattr(self, custom_method_name): return getattr(self, custom_method_name)(**kwargs) @api.multi def s2s_get_tx_status(self): """ Get the tx status. """ invalid_param_method_name = '_%s_s2s_get_tx_status' % self.acquirer_id.provider if hasattr(self, invalid_param_method_name): return getattr(self, invalid_param_method_name)() return True @api.multi def execute_callback(self): res = None for transaction in self: # limited sudo env, only for checking callback presence, not for running it! # manual transactions have no callback, and can pass without being run by admin user tx_sudo = transaction.sudo() if not (tx_sudo.callback_model_id and tx_sudo.callback_res_id and tx_sudo.callback_method): continue valid_token = transaction._generate_callback_hash() if not consteq(ustr(valid_token), transaction.callback_hash): _logger.warning( "Invalid callback signature for transaction %d" % (transaction.id)) continue record = self.env[transaction.callback_model_id.model].browse( transaction.callback_res_id).exists() if record: res = getattr(record, transaction.callback_method)(transaction) else: _logger.warning( "Did not found record %s.%s for callback of transaction %d" % (transaction.callback_model_id.model, transaction.callback_res_id, transaction.id)) return res @api.multi def action_capture(self): if any(self.mapped(lambda tx: tx.state != 'authorized')): raise ValidationError( _('Only transactions in the Authorized status can be captured.' )) for tx in self: tx.s2s_capture_transaction() @api.multi def action_void(self): if any(self.mapped(lambda tx: tx.state != 'authorized')): raise ValidationError( _('Only transactions in the Authorized status can be voided.')) for tx in self: tx.s2s_void_transaction()
class PayslipReport(models.Model): _name = "payslip.report" _description = "Payslip Analysis" _auto = False name = fields.Char(readonly=True) date_from = fields.Date(string='Date From', readonly=True) date_to = fields.Date(string='Date To', readonly=True) year = fields.Char(size=4, readonly=True) month = fields.Selection([('01', 'January'), ('02', 'February'), ('03', 'March'), ('04', 'April'), ('05', 'May'), ('06', 'June'), ('07', 'July'), ('08', 'August'), ('09', 'September'), ('10', 'October'), ('11', 'November'), ('12', 'December')], readonly=True) day = fields.Char(size=128, readonly=True) state = fields.Selection([ ('draft', 'Draft'), ('done', 'Done'), ('cancel', 'Rejected'), ], string='Status', readonly=True) employee_id = fields.Many2one('hr.employee', string='Employee', readonly=True) nbr = fields.Integer(string='# Payslip lines', readonly=True) number = fields.Char(readonly=True) struct_id = fields.Many2one('hr.payroll.structure', string='Structure', readonly=True) company_id = fields.Many2one('res.company', string='Company', readonly=True) paid = fields.Boolean(string='Made Payment Order ? ', readonly=True) total = fields.Float(readonly=True) category_id = fields.Many2one('hr.salary.rule.category', string='Category', readonly=True) @api.model_cr def init(self): drop_view_if_exists(self.env.cr, self._table) self.env.cr.execute(""" create or replace view payslip_report as ( select min(l.id) as id, l.name, p.struct_id, p.state, p.date_from, p.date_to, p.number, p.company_id, p.paid, l.category_id, l.employee_id, sum(l.total) as total, to_char(p.date_from, 'YYYY') as year, to_char(p.date_from, 'MM') as month, to_char(p.date_from, 'YYYY-MM-DD') as day, to_char(p.date_to, 'YYYY') as to_year, to_char(p.date_to, 'MM') as to_month, to_char(p.date_to, 'YYYY-MM-DD') as to_day, 1 AS nbr from hr_payslip as p left join hr_payslip_line as l on (p.id=l.slip_id) where l.employee_id IS NOT NULL group by p.number,l.name,p.date_from,p.date_to,p.state,p.company_id,p.paid, l.employee_id,p.struct_id,l.category_id ) """)
class AccountInvoiceReport(models.Model): _name = "account.invoice.report" _inherit = ['ir.branch.company.mixin'] _description = "Invoices Statistics" _auto = False _rec_name = 'date' @api.multi @api.depends('currency_id', 'date', 'price_total', 'price_average', 'residual') def _compute_amounts_in_user_currency(self): """Compute the amounts in the currency of the user """ context = dict(self._context or {}) user_currency_id = self.env.user.company_id.currency_id currency_rate_id = self.env['res.currency.rate'].search( [('rate', '=', 1), '|', ('company_id', '=', self.env.user.company_id.id), ('company_id', '=', False)], limit=1) base_currency_id = currency_rate_id.currency_id ctx = context.copy() for record in self: ctx['date'] = record.date record.user_currency_price_total = base_currency_id.with_context( ctx).compute(record.price_total, user_currency_id) record.user_currency_price_average = base_currency_id.with_context( ctx).compute(record.price_average, user_currency_id) record.user_currency_residual = base_currency_id.with_context( ctx).compute(record.residual, user_currency_id) date = fields.Date(readonly=True) product_id = fields.Many2one('product.product', string='Product', readonly=True) product_qty = fields.Float(string='Product Quantity', readonly=True) uom_name = fields.Char(string='Reference Unit of Measure', readonly=True) payment_term_id = fields.Many2one('account.payment.term', string='Payment Terms', oldname='payment_term', readonly=True) fiscal_position_id = fields.Many2one('account.fiscal.position', oldname='fiscal_position', string='Fiscal Position', readonly=True) currency_id = fields.Many2one('res.currency', string='Currency', readonly=True) categ_id = fields.Many2one('product.category', string='Product Category', readonly=True) journal_id = fields.Many2one('account.journal', string='Journal', readonly=True) partner_id = fields.Many2one('res.partner', string='Partner', readonly=True) commercial_partner_id = fields.Many2one('res.partner', string='Partner Company', help="Commercial Entity") company_id = fields.Many2one('res.company', string='Company', readonly=True) user_id = fields.Many2one('res.users', string='Salesperson', readonly=True) price_total = fields.Float(string='Total Without Tax', readonly=True) user_currency_price_total = fields.Float( string="Total Without Tax", compute='_compute_amounts_in_user_currency', digits=0) price_average = fields.Float(string='Average Price', readonly=True, group_operator="avg") user_currency_price_average = fields.Float( string="Average Price", compute='_compute_amounts_in_user_currency', digits=0) currency_rate = fields.Float(string='Currency Rate', readonly=True, group_operator="avg", groups="base.group_multi_currency") nbr = fields.Integer( string='# of Lines', readonly=True) # TDE FIXME master: rename into nbr_lines type = fields.Selection([ ('out_invoice', 'Customer Invoice'), ('in_invoice', 'Vendor Bill'), ('out_refund', 'Customer Credit Note'), ('in_refund', 'Vendor Credit Note'), ], readonly=True) state = fields.Selection([('draft', 'Draft'), ('open', 'Open'), ('paid', 'Paid'), ('cancel', 'Cancelled')], string='Invoice Status', readonly=True) date_due = fields.Date(string='Due Date', readonly=True) account_id = fields.Many2one('account.account', string='Account', readonly=True, domain=[('deprecated', '=', False)]) account_line_id = fields.Many2one('account.account', string='Account Line', readonly=True, domain=[('deprecated', '=', False)]) partner_bank_id = fields.Many2one('res.partner.bank', string='Bank Account', readonly=True) residual = fields.Float(string='Due Amount', readonly=True) user_currency_residual = fields.Float( string="Total Residual", compute='_compute_amounts_in_user_currency', digits=0) country_id = fields.Many2one('res.country', string='Country of the Partner Company') account_analytic_id = fields.Many2one( 'account.analytic.account', string='Analytic Account', groups="analytic.group_analytic_accounting") _order = 'date desc' _depends = { 'account.invoice': [ 'account_id', 'amount_total_company_signed', 'commercial_partner_id', 'company_id', 'branch_id', 'currency_id', 'date_due', 'date_invoice', 'fiscal_position_id', 'journal_id', 'partner_bank_id', 'partner_id', 'payment_term_id', 'residual', 'state', 'type', 'user_id', ], 'account.invoice.line': [ 'account_id', 'invoice_id', 'price_subtotal', 'product_id', 'quantity', 'uom_id', 'account_analytic_id', ], 'product.product': ['product_tmpl_id'], 'product.template': ['categ_id'], 'product.uom': ['category_id', 'factor', 'name', 'uom_type'], 'res.currency.rate': ['currency_id', 'name'], 'res.partner': ['country_id'], } def _select(self): select_str = """ SELECT sub.id, sub.date, sub.product_id, sub.partner_id, sub.country_id, sub.account_analytic_id, sub.payment_term_id, sub.uom_name, sub.currency_id, sub.journal_id, sub.fiscal_position_id, sub.user_id, sub.company_id, sub.branch_id, sub.nbr, sub.type, sub.state, sub.categ_id, sub.date_due, sub.account_id, sub.account_line_id, sub.partner_bank_id, sub.product_qty, sub.price_total as price_total, sub.price_average as price_average, COALESCE(cr.rate, 1) as currency_rate, sub.residual as residual, sub.commercial_partner_id as commercial_partner_id """ return select_str def _sub_select(self): select_str = """ SELECT ail.id AS id, ai.date_invoice AS date, ail.product_id, ai.partner_id, ai.payment_term_id, ail.account_analytic_id, u2.name AS uom_name, ai.currency_id, ai.journal_id, ai.fiscal_position_id, ai.user_id, ai.company_id, ai.branch_id, 1 AS nbr, ai.type, ai.state, pt.categ_id, ai.date_due, ai.account_id, ail.account_id AS account_line_id, ai.partner_bank_id, SUM ((invoice_type.sign * ail.quantity) / u.factor * u2.factor) AS product_qty, SUM(ail.price_subtotal_signed * invoice_type.sign) AS price_total, SUM(ABS(ail.price_subtotal_signed)) / CASE WHEN SUM(ail.quantity / u.factor * u2.factor) <> 0::numeric THEN SUM(ail.quantity / u.factor * u2.factor) ELSE 1::numeric END AS price_average, ai.residual_company_signed / (SELECT count(*) FROM account_invoice_line l where invoice_id = ai.id) * count(*) * invoice_type.sign AS residual, ai.commercial_partner_id as commercial_partner_id, partner.country_id """ return select_str def _from(self): from_str = """ FROM account_invoice_line ail JOIN account_invoice ai ON ai.id = ail.invoice_id JOIN res_partner partner ON ai.commercial_partner_id = partner.id LEFT JOIN product_product pr ON pr.id = ail.product_id left JOIN product_template pt ON pt.id = pr.product_tmpl_id LEFT JOIN product_uom u ON u.id = ail.uom_id LEFT JOIN product_uom u2 ON u2.id = pt.uom_id JOIN ( -- Temporary table to decide if the qty should be added or retrieved (Invoice vs Credit Note) SELECT id,(CASE WHEN ai.type::text = ANY (ARRAY['in_refund'::character varying::text, 'in_invoice'::character varying::text]) THEN -1 ELSE 1 END) AS sign FROM account_invoice ai ) AS invoice_type ON invoice_type.id = ai.id """ return from_str def _group_by(self): group_by_str = """ GROUP BY ail.id, ail.product_id, ail.account_analytic_id, ai.date_invoice, ai.id, ai.partner_id, ai.payment_term_id, u2.name, u2.id, ai.currency_id, ai.journal_id, ai.fiscal_position_id, ai.user_id, ai.company_id, ai.branch_id, ai.type, invoice_type.sign, ai.state, pt.categ_id, ai.date_due, ai.account_id, ail.account_id, ai.partner_bank_id, ai.residual_company_signed, ai.amount_total_company_signed, ai.commercial_partner_id, partner.country_id """ return group_by_str @api.model_cr def init(self): # self._table = account_invoice_report tools.drop_view_if_exists(self.env.cr, self._table) self.env.cr.execute( """CREATE or REPLACE VIEW %s as ( WITH currency_rate AS (%s) %s FROM ( %s %s %s ) AS sub LEFT JOIN currency_rate cr ON (cr.currency_id = sub.currency_id AND cr.company_id = sub.company_id AND cr.date_start <= COALESCE(sub.date, NOW()) AND (cr.date_end IS NULL OR cr.date_end > COALESCE(sub.date, NOW()))) )""" % (self._table, self.env['res.currency']._select_companies_rates(), self._select(), self._sub_select(), self._from(), self._group_by()))
class PurchaseRequisition(models.Model): _name = "purchase.requisition" _description = "Purchase Requisition" _inherit = ['mail.thread'] _order = "id desc" def _get_picking_in(self): pick_in = self.env.ref('stock.picking_type_in') if not pick_in: company = self.env['res.company']._company_default_get( 'purchase.requisition') pick_in = self.env['stock.picking.type'].search( [('warehouse_id.company_id', '=', company.id), ('code', '=', 'incoming')], limit=1, ) return pick_in def _get_type_id(self): return self.env['purchase.requisition.type'].search([], limit=1) name = fields.Char(string='Agreement Reference', required=True, copy=False, default=lambda self: self.env['ir.sequence']. next_by_code('purchase.order.requisition')) origin = fields.Char(string='Source Document') order_count = fields.Integer(compute='_compute_orders_number', string='Number of Orders') vendor_id = fields.Many2one('res.partner', string="Vendor") type_id = fields.Many2one('purchase.requisition.type', string="Agreement Type", required=True, default=_get_type_id) ordering_date = fields.Date(string="Ordering Date") date_end = fields.Datetime(string='Agreement Deadline') schedule_date = fields.Date( string='Delivery Date', index=True, help= "The expected and scheduled delivery date where all the products are received" ) user_id = fields.Many2one('res.users', string='Responsible', default=lambda self: self.env.user) description = fields.Text() company_id = fields.Many2one('res.company', string='Company', required=True, default=lambda self: self.env['res.company']. _company_default_get('purchase.requisition')) purchase_ids = fields.One2many('purchase.order', 'requisition_id', string='Purchase Orders', states={'done': [('readonly', True)]}) line_ids = fields.One2many('purchase.requisition.line', 'requisition_id', string='Products to Purchase', states={'done': [('readonly', True)]}, copy=True) warehouse_id = fields.Many2one('stock.warehouse', string='Warehouse') state = fields.Selection([('draft', 'Draft'), ('in_progress', 'Confirmed'), ('open', 'Bid Selection'), ('done', 'Done'), ('cancel', 'Cancelled')], 'Status', track_visibility='onchange', required=True, copy=False, default='draft') account_analytic_id = fields.Many2one('account.analytic.account', 'Analytic Account') picking_type_id = fields.Many2one('stock.picking.type', 'Operation Type', required=True, default=_get_picking_in) @api.multi @api.depends('purchase_ids') def _compute_orders_number(self): for requisition in self: requisition.order_count = len(requisition.purchase_ids) @api.multi def action_cancel(self): # try to set all associated quotations to cancel state for requisition in self: requisition.purchase_ids.button_cancel() for po in requisition.purchase_ids: po.message_post(body=_( 'Cancelled by the agreement associated to this quotation.') ) self.write({'state': 'cancel'}) @api.multi def action_in_progress(self): if not all(obj.line_ids for obj in self): raise UserError( _('You cannot confirm call because there is no product line.')) self.write({'state': 'in_progress'}) @api.multi def action_open(self): self.write({'state': 'open'}) @api.multi def action_draft(self): self.write({'state': 'draft'}) @api.multi def action_done(self): """ Generate all purchase order based on selected lines, should only be called on one agreement at a time """ if any(purchase_order.state in ['draft', 'sent', 'to approve'] for purchase_order in self.mapped('purchase_ids')): raise UserError( _('You have to cancel or validate every RfQ before closing the purchase requisition.' )) self.write({'state': 'done'}) def _prepare_tender_values(self, product_id, product_qty, product_uom, location_id, name, origin, values): return { 'origin': origin, 'date_end': values['date_planned'], 'warehouse_id': values.get('warehouse_id') and values['warehouse_id'].id or False, 'company_id': values['company_id'].id, 'line_ids': [(0, 0, { 'product_id': product_id.id, 'product_uom_id': product_uom.id, 'product_qty': product_qty, 'move_dest_id': values.get('move_dest_ids') and values['move_dest_ids'][0].id or False, })], }
class LunchOrderLine(models.Model): _name = 'lunch.order.line' _description = 'lunch order line' _order = 'date desc, id desc' name = fields.Char(related='product_id.name', string="Product Name", readonly=True) order_id = fields.Many2one('lunch.order', 'Order', ondelete='cascade', required=True) product_id = fields.Many2one('lunch.product', 'Product', required=True) category_id = fields.Many2one('lunch.product.category', string='Product Category', related='product_id.category_id', readonly=True, store=True) date = fields.Date(string='Date', related='order_id.date', readonly=True, store=True) supplier = fields.Many2one('res.partner', string='Vendor', related='product_id.supplier', readonly=True, store=True) user_id = fields.Many2one('res.users', string='User', related='order_id.user_id', readonly=True, store=True) note = fields.Text('Note') price = fields.Float(related='product_id.price', readonly=True, store=True, digits=dp.get_precision('Account')) state = fields.Selection([('new', 'New'), ('confirmed', 'Received'), ('ordered', 'Ordered'), ('cancelled', 'Cancelled')], 'Status', readonly=True, index=True, default='new') cashmove = fields.One2many('lunch.cashmove', 'order_id', 'Cash Move') currency_id = fields.Many2one('res.currency', related='order_id.currency_id') @api.one def order(self): """ The order_line is ordered to the vendor but isn't received yet """ if self.user_has_groups("lunch.group_lunch_manager"): self.state = 'ordered' else: raise AccessError( _("Only your lunch manager processes the orders.")) @api.one def confirm(self): """ confirm one or more order line, update order status and create new cashmove """ if self.user_has_groups("lunch.group_lunch_manager"): if self.state != 'confirmed': values = { 'user_id': self.user_id.id, 'amount': -self.price, 'description': self.product_id.name, 'order_id': self.id, 'state': 'order', 'date': self.date, } self.env['lunch.cashmove'].create(values) self.state = 'confirmed' else: raise AccessError( _("Only your lunch manager sets the orders as received.")) @api.one def cancel(self): """ cancel one or more order.line, update order status and unlink existing cashmoves """ if self.user_has_groups("lunch.group_lunch_manager"): self.state = 'cancelled' self.cashmove.unlink() else: raise AccessError(_("Only your lunch manager cancels the orders."))
class ProductAttribute(models.Model): _inherit = 'product.attribute' category_id = fields.Many2one('product.attribute.category', string="Category", help="Set a category to regroup similar attributes under " "the same section in the Comparison page of eCommerce")
class PickingType(models.Model): _name = "stock.picking.type" _description = "The operation type determines the picking view" _order = 'sequence, id' name = fields.Char('Operation Types Name', required=True, translate=True) color = fields.Integer('Color') sequence = fields.Integer('Sequence', help="Used to order the 'All Operations' kanban view") sequence_id = fields.Many2one('ir.sequence', 'Reference Sequence', required=True) default_location_src_id = fields.Many2one( 'stock.location', 'Default Source Location', help="This is the default source location when you create a picking manually with this operation type. It is possible however to change it or that the routes put another location. If it is empty, it will check for the supplier location on the partner. ") default_location_dest_id = fields.Many2one( 'stock.location', 'Default Destination Location', help="This is the default destination location when you create a picking manually with this operation type. It is possible however to change it or that the routes put another location. If it is empty, it will check for the customer location on the partner. ") code = fields.Selection([('incoming', 'Vendors'), ('outgoing', 'Customers'), ('internal', 'Internal')], 'Type of Operation', required=True) return_picking_type_id = fields.Many2one('stock.picking.type', 'Operation Type for Returns') show_entire_packs = fields.Boolean('Allow moving packs', help="If checked, this shows the packs to be moved as a whole in the Operations tab all the time, even if there was no entire pack reserved.") warehouse_id = fields.Many2one( 'stock.warehouse', 'Warehouse', ondelete='cascade', default=lambda self: self.env['stock.warehouse'].search([('company_id', '=', self.env.user.company_id.id)], limit=1)) active = fields.Boolean('Active', default=True) use_create_lots = fields.Boolean( 'Create New Lots/Serial Numbers', default=True, help="If this is checked only, it will suppose you want to create new Lots/Serial Numbers, so you can provide them in a text field. ") use_existing_lots = fields.Boolean( 'Use Existing Lots/Serial Numbers', default=True, help="If this is checked, you will be able to choose the Lots/Serial Numbers. You can also decide to not put lots in this operation type. This means it will create stock with no lot or not put a restriction on the lot taken. ") show_operations = fields.Boolean( 'Show Detailed Operations', default=False, help="If this checkbox is ticked, the pickings lines will represent detailed stock operations. If not, the picking lines will represent an aggregate of detailed stock operations.") show_reserved = fields.Boolean( 'Show Reserved', default=True, help="If this checkbox is ticked, actpy will show which products are reserved (lot/serial number, source location, source package).") # Statistics for the kanban view last_done_picking = fields.Char('Last 10 Done Pickings', compute='_compute_last_done_picking') count_picking_draft = fields.Integer(compute='_compute_picking_count') count_picking_ready = fields.Integer(compute='_compute_picking_count') count_picking = fields.Integer(compute='_compute_picking_count') count_picking_waiting = fields.Integer(compute='_compute_picking_count') count_picking_late = fields.Integer(compute='_compute_picking_count') count_picking_backorders = fields.Integer(compute='_compute_picking_count') rate_picking_late = fields.Integer(compute='_compute_picking_count') rate_picking_backorders = fields.Integer(compute='_compute_picking_count') barcode_nomenclature_id = fields.Many2one( 'barcode.nomenclature', 'Barcode Nomenclature') @api.one def _compute_last_done_picking(self): # TDE TODO: true multi tristates = [] for picking in self.env['stock.picking'].search([('picking_type_id', '=', self.id), ('state', '=', 'done')], order='date_done desc', limit=10): if picking.date_done > picking.date: tristates.insert(0, {'tooltip': picking.name or '' + ": " + _('Late'), 'value': -1}) elif picking.backorder_id: tristates.insert(0, {'tooltip': picking.name or '' + ": " + _('Backorder exists'), 'value': 0}) else: tristates.insert(0, {'tooltip': picking.name or '' + ": " + _('OK'), 'value': 1}) self.last_done_picking = json.dumps(tristates) def _compute_picking_count(self): # TDE TODO count picking can be done using previous two domains = { 'count_picking_draft': [('state', '=', 'draft')], 'count_picking_waiting': [('state', 'in', ('confirmed', 'waiting'))], 'count_picking_ready': [('state', '=', 'assigned')], 'count_picking': [('state', 'in', ('assigned', 'waiting', 'confirmed'))], 'count_picking_late': [('scheduled_date', '<', time.strftime(DEFAULT_SERVER_DATETIME_FORMAT)), ('state', 'in', ('assigned', 'waiting', 'confirmed'))], 'count_picking_backorders': [('backorder_id', '!=', False), ('state', 'in', ('confirmed', 'assigned', 'waiting'))], } for field in domains: data = self.env['stock.picking'].read_group(domains[field] + [('state', 'not in', ('done', 'cancel')), ('picking_type_id', 'in', self.ids)], ['picking_type_id'], ['picking_type_id']) count = { x['picking_type_id'][0]: x['picking_type_id_count'] for x in data if x['picking_type_id'] } for record in self: record[field] = count.get(record.id, 0) for record in self: record.rate_picking_late = record.count_picking and record.count_picking_late * 100 / record.count_picking or 0 record.rate_picking_backorders = record.count_picking and record.count_picking_backorders * 100 / record.count_picking or 0 def name_get(self): """ Display 'Warehouse_name: PickingType_name' """ # TDE TODO remove context key support + update purchase res = [] for picking_type in self: if self.env.context.get('special_shortened_wh_name'): if picking_type.warehouse_id: name = picking_type.warehouse_id.name else: name = _('Customer') + ' (' + picking_type.name + ')' elif picking_type.warehouse_id: name = picking_type.warehouse_id.name + ': ' + picking_type.name else: name = picking_type.name res.append((picking_type.id, name)) return res @api.model def name_search(self, name, args=None, operator='ilike', limit=100): args = args or [] domain = [] if name: domain = ['|', ('name', operator, name), ('warehouse_id.name', operator, name)] picks = self.search(domain + args, limit=limit) return picks.name_get() @api.onchange('code') def onchange_picking_code(self): if self.code == 'incoming': self.default_location_src_id = self.env.ref('stock.stock_location_suppliers').id self.default_location_dest_id = self.env.ref('stock.stock_location_stock').id elif self.code == 'outgoing': self.default_location_src_id = self.env.ref('stock.stock_location_stock').id self.default_location_dest_id = self.env.ref('stock.stock_location_customers').id @api.onchange('show_operations') def onchange_show_operations(self): if self.show_operations is True: self.show_reserved = True def _get_action(self, action_xmlid): # TDE TODO check to have one view + custo in methods action = self.env.ref(action_xmlid).read()[0] if self: action['display_name'] = self.display_name return action def get_action_picking_tree_late(self): return self._get_action('stock.action_picking_tree_late') def get_action_picking_tree_backorder(self): return self._get_action('stock.action_picking_tree_backorder') def get_action_picking_tree_waiting(self): return self._get_action('stock.action_picking_tree_waiting') def get_action_picking_tree_ready(self): return self._get_action('stock.action_picking_tree_ready') def get_stock_picking_action_picking_type(self): return self._get_action('stock.stock_picking_action_picking_type')
class MrpBom(models.Model): """ Defines bills of material for a product or a product template """ _name = 'mrp.bom' _description = 'Bill of Material' _inherit = ['mail.thread'] _rec_name = 'product_tmpl_id' _order = "sequence" def _get_default_product_uom_id(self): return self.env['product.uom'].search([], limit=1, order='id').id code = fields.Char('Reference') active = fields.Boolean( 'Active', default=True, help= "If the active field is set to False, it will allow you to hide the bills of material without removing it." ) type = fields.Selection([('normal', 'Manufacture this product'), ('phantom', 'Kit')], 'BoM Type', default='normal', required=True) product_tmpl_id = fields.Many2one( 'product.template', 'Product', domain="[('type', 'in', ['product', 'consu'])]", required=True) product_id = fields.Many2one( 'product.product', 'Product Variant', domain= "['&', ('product_tmpl_id', '=', product_tmpl_id), ('type', 'in', ['product', 'consu'])]", help= "If a product variant is defined the BOM is available only for this product." ) bom_line_ids = fields.One2many('mrp.bom.line', 'bom_id', 'BoM Lines', copy=True) product_qty = fields.Float('Quantity', default=1.0, digits=dp.get_precision('Unit of Measure'), required=True) product_uom_id = fields.Many2one( 'product.uom', 'Product Unit of Measure', default=_get_default_product_uom_id, oldname='product_uom', required=True, help= "Unit of Measure (Unit of Measure) is the unit of measurement for the inventory control" ) sequence = fields.Integer( 'Sequence', help= "Gives the sequence order when displaying a list of bills of material." ) routing_id = fields.Many2one( 'mrp.routing', 'Routing', help= "The operations for producing this BoM. When a routing is specified, the production orders will " " be executed through work orders, otherwise everything is processed in the production order itself. " ) ready_to_produce = fields.Selection( [('all_available', 'All components available'), ('asap', 'The components of 1st operation')], string='Manufacturing Readiness', default='asap', required=True) picking_type_id = fields.Many2one( 'stock.picking.type', 'Operation Type', domain=[('code', '=', 'mrp_operation')], help= u"When a procurement has a ‘produce’ route with a operation type set, it will try to create " "a Manufacturing Order for that product using a BoM of the same operation type. That allows " "to define procurement rules which trigger different manufacturing orders with different BoMs." ) company_id = fields.Many2one('res.company', 'Company', default=lambda self: self.env['res.company']. _company_default_get('mrp.bom'), required=True) @api.constrains('product_id', 'product_tmpl_id', 'bom_line_ids') def _check_product_recursion(self): for bom in self: if bom.bom_line_ids.filtered(lambda x: x.product_id.product_tmpl_id == bom.product_tmpl_id): raise ValidationError( _('BoM line product %s should not be same as BoM product.') % bom.display_name) @api.onchange('product_uom_id') def onchange_product_uom_id(self): res = {} if not self.product_uom_id or not self.product_tmpl_id: return if self.product_uom_id.category_id.id != self.product_tmpl_id.uom_id.category_id.id: self.product_uom_id = self.product_tmpl_id.uom_id.id res['warning'] = { 'title': _('Warning'), 'message': _('The Product Unit of Measure you chose has a different category than in the product form.' ) } return res @api.onchange('product_tmpl_id') def onchange_product_tmpl_id(self): if self.product_tmpl_id: self.product_uom_id = self.product_tmpl_id.uom_id.id @api.onchange('routing_id') def onchange_routing_id(self): for line in self.bom_line_ids: line.operation_id = False @api.multi def name_get(self): return [(bom.id, '%s%s' % (bom.code and '%s: ' % bom.code or '', bom.product_tmpl_id.display_name)) for bom in self] @api.multi def unlink(self): if self.env['mrp.production'].search( [('bom_id', 'in', self.ids), ('state', 'not in', ['done', 'cancel'])], limit=1): raise UserError( _('You can not delete a Bill of Material with running manufacturing orders.\nPlease close or cancel it first.' )) return super(MrpBom, self).unlink() @api.model def _bom_find(self, product_tmpl=None, product=None, picking_type=None, company_id=False): """ Finds BoM for particular product, picking and company """ if product: if not product_tmpl: product_tmpl = product.product_tmpl_id domain = [ '|', ('product_id', '=', product.id), '&', ('product_id', '=', False), ('product_tmpl_id', '=', product_tmpl.id) ] elif product_tmpl: domain = [('product_tmpl_id', '=', product_tmpl.id)] else: # neither product nor template, makes no sense to search return False if picking_type: domain += [ '|', ('picking_type_id', '=', picking_type.id), ('picking_type_id', '=', False) ] if company_id or self.env.context.get('company_id'): domain = domain + [('company_id', '=', company_id or self.env.context.get('company_id'))] # order to prioritize bom with product_id over the one without return self.search(domain, order='sequence, product_id', limit=1) def explode(self, product, quantity, picking_type=False): """ Explodes the BoM and creates two lists with all the information you need: bom_done and line_done Quantity describes the number of times you need the BoM: so the quantity divided by the number created by the BoM and converted into its UoM """ from collections import defaultdict graph = defaultdict(list) V = set() def check_cycle(v, visited, recStack, graph): visited[v] = True recStack[v] = True for neighbour in graph[v]: if visited[neighbour] == False: if check_cycle(neighbour, visited, recStack, graph) == True: return True elif recStack[neighbour] == True: return True recStack[v] = False return False boms_done = [(self, { 'qty': quantity, 'product': product, 'original_qty': quantity, 'parent_line': False })] lines_done = [] V |= set([product.product_tmpl_id.id]) bom_lines = [(bom_line, product, quantity, False) for bom_line in self.bom_line_ids] for bom_line in self.bom_line_ids: V |= set([bom_line.product_id.product_tmpl_id.id]) graph[product.product_tmpl_id.id].append( bom_line.product_id.product_tmpl_id.id) while bom_lines: current_line, current_product, current_qty, parent_line = bom_lines[ 0] bom_lines = bom_lines[1:] if current_line._skip_bom_line(current_product): continue line_quantity = current_qty * current_line.product_qty bom = self._bom_find(product=current_line.product_id, picking_type=picking_type or self.picking_type_id, company_id=self.company_id.id) if bom.type == 'phantom': converted_line_quantity = current_line.product_uom_id._compute_quantity( line_quantity / bom.product_qty, bom.product_uom_id) bom_lines = [(line, current_line.product_id, converted_line_quantity, current_line) for line in bom.bom_line_ids] + bom_lines for bom_line in bom.bom_line_ids: graph[current_line.product_id.product_tmpl_id.id].append( bom_line.product_id.product_tmpl_id.id) if bom_line.product_id.product_tmpl_id.id in V and check_cycle( bom_line.product_id.product_tmpl_id.id, {key: False for key in V}, {key: False for key in V}, graph): raise UserError( _('Recursion error! A product with a Bill of Material should not have itself in its BoM or child BoMs!' )) V |= set([bom_line.product_id.product_tmpl_id.id]) boms_done.append((bom, { 'qty': converted_line_quantity, 'product': current_product, 'original_qty': quantity, 'parent_line': current_line })) else: # We round up here because the user expects that if he has to consume a little more, the whole UOM unit # should be consumed. rounding = current_line.product_uom_id.rounding line_quantity = float_round(line_quantity, precision_rounding=rounding, rounding_method='UP') lines_done.append((current_line, { 'qty': line_quantity, 'product': current_product, 'original_qty': quantity, 'parent_line': parent_line })) return boms_done, lines_done
class BarnchConfiguration(models.TransientModel): _name = 'branch.config' name = fields.Char(string='Name', required=True) code = fields.Char(string='Code', required=True) branch_id = fields.Many2one('res.branch', 'Branch') company_id = fields.Many2one('res.company', string="Company", default=lambda self: self.env.user.company_id, required=True) partner_id = fields.Many2one('res.partner', string='Partner') street = fields.Char() street2 = fields.Char() zip = fields.Char(change_default=True) city = fields.Char() state_id = fields.Many2one("res.country.state", string='State', ondelete='restrict') country_id = fields.Many2one('res.country', string='Country', ondelete='restrict') email = fields.Char() phone = fields.Char() mobile = fields.Char() state = fields.Selection([('draft', 'Draft'), ('confirm', 'Confirm')], default='draft') user_ids = fields.Many2many('res.users', 'res_users_branch_rel', 'user_id', 'branch_id', 'Allowed Branch for users', domain="[('company_id','=',company_id)]") default_user_ids = fields.Many2many('res.users', 'res_users_branch_default_rel', 'user_id', 'branch_id', 'Default Branch for users', domain="[('company_id','=',company_id)]") @api.multi def branch_config(self): s_ids = self.search_read([('id', '=', self.id)], [])[0] branch = self.env['res.branch'].create({ 'name': s_ids['name'], 'code': s_ids['code'], 'street': s_ids['street'], 'street2': s_ids['street2'], 'zip': s_ids['zip'], 'city': s_ids['city'], 'state_id': s_ids['state_id'] and s_ids['state_id'][0], 'country_id': s_ids['country_id'] and s_ids['country_id'][0], 'email': s_ids['email'], 'phone': s_ids['phone'], 'company_id': s_ids['company_id'] and s_ids['company_id'][0], 'mobile': s_ids['mobile'], }) self.write({'state': 'confirm', 'partner_id': branch.partner_id.id, 'branch_id': branch.id}) view_id = self.env.ref( 'base_branch_company.view_branch_config') context = dict(self._context) return {'views': [(view_id.id, 'form')], 'view_id': view_id.id, 'type': 'ir.actions.act_window', 'view_type': 'form', 'view_mode': 'form', 'res_model': 'branch.config', 'target': 'new', 'res_id': self.id, 'context': context, } @api.multi def finish_branch_config(self): for user_id in self.user_ids: user_id.write({'branch_ids': [(4, self.branch_id.id)]}) for user_id in self.user_ids: user_id.write({'default_branch_id': self.branch_id.id})
class MrpBomLine(models.Model): _name = 'mrp.bom.line' _order = "sequence, id" _rec_name = "product_id" def _get_default_product_uom_id(self): return self.env['product.uom'].search([], limit=1, order='id').id product_id = fields.Many2one('product.product', 'Product', required=True) product_qty = fields.Float( 'Product Quantity', default=1.0, digits=dp.get_precision('Product Unit of Measure'), required=True) product_uom_id = fields.Many2one( 'product.uom', 'Product Unit of Measure', default=_get_default_product_uom_id, oldname='product_uom', required=True, help= "Unit of Measure (Unit of Measure) is the unit of measurement for the inventory control" ) sequence = fields.Integer('Sequence', default=1, help="Gives the sequence order when displaying.") routing_id = fields.Many2one( 'mrp.routing', 'Routing', related='bom_id.routing_id', store=True, help= "The list of operations 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.") bom_id = fields.Many2one('mrp.bom', 'Parent BoM', index=True, ondelete='cascade', required=True) attribute_value_ids = fields.Many2many( 'product.attribute.value', string='Variants', help="BOM Product Variants needed form apply this line.") operation_id = fields.Many2one( 'mrp.routing.workcenter', 'Consumed in Operation', help= "The operation where the components are consumed, or the finished products created." ) child_bom_id = fields.Many2one('mrp.bom', 'Sub BoM', compute='_compute_child_bom_id') child_line_ids = fields.One2many('mrp.bom.line', string="BOM lines of the referred bom", compute='_compute_child_line_ids') has_attachments = fields.Boolean('Has Attachments', compute='_compute_has_attachments') _sql_constraints = [ ('bom_qty_zero', 'CHECK (product_qty>=0)', 'All product quantities must be greater or equal to 0.\n' 'Lines with 0 quantities can be used as optional lines. \n' 'You should install the mrp_byproduct module if you want to manage extra products on BoMs !' ), ] @api.one @api.depends('product_id', 'bom_id') def _compute_child_bom_id(self): if not self.product_id: self.child_bom_id = False else: self.child_bom_id = self.env['mrp.bom']._bom_find( product_tmpl=self.product_id.product_tmpl_id, product=self.product_id, picking_type=self.bom_id.picking_type_id) @api.one @api.depends('product_id') def _compute_has_attachments(self): nbr_attach = self.env['ir.attachment'].search_count([ '|', '&', ('res_model', '=', 'product.product'), ('res_id', '=', self.product_id.id), '&', ('res_model', '=', 'product.template'), ('res_id', '=', self.product_id.product_tmpl_id.id) ]) self.has_attachments = bool(nbr_attach) @api.one @api.depends('child_bom_id') def _compute_child_line_ids(self): """ If the BOM line refers to a BOM, return the ids of the child BOM lines """ self.child_line_ids = self.child_bom_id.bom_line_ids.ids @api.onchange('product_uom_id') def onchange_product_uom_id(self): res = {} if not self.product_uom_id or not self.product_id: return res if self.product_uom_id.category_id != self.product_id.uom_id.category_id: self.product_uom_id = self.product_id.uom_id.id res['warning'] = { 'title': _('Warning'), 'message': _('The Product Unit of Measure you chose has a different category than in the product form.' ) } return res @api.onchange('product_id') def onchange_product_id(self): if self.product_id: self.product_uom_id = self.product_id.uom_id.id @api.model def create(self, values): if 'product_id' in values and 'product_uom_id' not in values: values['product_uom_id'] = self.env['product.product'].browse( values['product_id']).uom_id.id return super(MrpBomLine, self).create(values) def _skip_bom_line(self, product): """ Control if a BoM line should be produce, can be inherited for add custom control. It currently checks that all variant values are in the product. """ if self.attribute_value_ids: if not product or self.attribute_value_ids - product.attribute_value_ids: return True return False @api.multi def action_see_attachments(self): domain = [ '|', '&', ('res_model', '=', 'product.product'), ('res_id', '=', self.product_id.id), '&', ('res_model', '=', 'product.template'), ('res_id', '=', self.product_id.product_tmpl_id.id) ] attachment_view = self.env.ref('mrp.view_document_file_kanban_mrp') return { 'name': _('Attachments'), 'domain': domain, 'res_model': 'mrp.document', 'type': 'ir.actions.act_window', 'view_id': attachment_view.id, 'views': [(attachment_view.id, 'kanban'), (False, 'form')], 'view_mode': 'kanban,tree,form', 'view_type': 'form', 'help': _('''<p class="oe_view_nocontent_create"> Click to upload files to your product. </p><p> Use this feature to store any files, like drawings or specifications. </p>'''), 'limit': 80, 'context': "{'default_res_model': '%s','default_res_id': %d}" % ('product.product', self.product_id.id) }
class MergeOpportunity(models.TransientModel): """ Merge opportunities together. If we're talking about opportunities, it's just because it makes more sense to merge opps than leads, because the leads are more ephemeral objects. But since opportunities are leads, it's also possible to merge leads together (resulting in a new lead), or leads and opps together (resulting in a new opp). """ _name = 'crm.merge.opportunity' _description = 'Merge opportunities' @api.model def default_get(self, fields): """ Use active_ids from the context to fetch the leads/opps to merge. In order to get merged, these leads/opps can't be in 'Dead' or 'Closed' """ record_ids = self._context.get('active_ids') result = super(MergeOpportunity, self).default_get(fields) if record_ids: if 'opportunity_ids' in fields: opp_ids = self.env['crm.lead'].browse(record_ids).filtered( lambda opp: opp.probability < 100).ids result['opportunity_ids'] = opp_ids return result opportunity_ids = fields.Many2many('crm.lead', 'merge_opportunity_rel', 'merge_id', 'opportunity_id', string='Leads/Opportunities') user_id = fields.Many2one('res.users', 'Salesperson', index=True) team_id = fields.Many2one('crm.team', 'Sales Channel', oldname='section_id', index=True) @api.multi def action_merge(self): self.ensure_one() merge_opportunity = self.opportunity_ids.merge_opportunity( self.user_id.id, self.team_id.id) # The newly created lead might be a lead or an opp: redirect toward the right view if merge_opportunity.type == 'opportunity': return merge_opportunity.redirect_opportunity_view() else: return merge_opportunity.redirect_lead_view() @api.onchange('user_id') def _onchange_user(self): """ When changing the user, also set a team_id or restrict team id to the ones user_id is member of. """ team_id = False if self.user_id: user_in_team = False if self.team_id: user_in_team = self.env['crm.team'].search_count([ ('id', '=', self.team_id.id), '|', ('user_id', '=', self.user_id.id), ('member_ids', '=', self.user_id.id) ]) if not user_in_team: team_id = self.env['crm.team'].search([ '|', ('user_id', '=', self.user_id.id), ('member_ids', '=', self.user_id.id) ], limit=1) self.team_id = team_id
class Lead2OpportunityPartner(models.TransientModel): _name = 'crm.lead2opportunity.partner' _description = 'Lead To Opportunity Partner' _inherit = 'crm.partner.binding' @api.model def default_get(self, fields): """ Default get for name, opportunity_ids. If there is an exisitng partner link to the lead, find all existing opportunities links with this partner to merge all information together """ result = super(Lead2OpportunityPartner, self).default_get(fields) if self._context.get('active_id'): tomerge = {int(self._context['active_id'])} partner_id = result.get('partner_id') lead = self.env['crm.lead'].browse(self._context['active_id']) email = lead.partner_id.email if lead.partner_id else lead.email_from tomerge.update( self._get_duplicated_leads(partner_id, email, include_lost=True).ids) if 'action' in fields and not result.get('action'): result['action'] = 'exist' if partner_id else 'create' if 'partner_id' in fields: result['partner_id'] = partner_id if 'name' in fields: result['name'] = 'merge' if len(tomerge) >= 2 else 'convert' if 'opportunity_ids' in fields and len(tomerge) >= 2: result['opportunity_ids'] = list(tomerge) if lead.user_id: result['user_id'] = lead.user_id.id if lead.team_id: result['team_id'] = lead.team_id.id if not partner_id and not lead.contact_name: result['action'] = 'nothing' return result name = fields.Selection([('convert', 'Convert to opportunity'), ('merge', 'Merge with existing opportunities')], 'Conversion Action', required=True) opportunity_ids = fields.Many2many('crm.lead', string='Opportunities') user_id = fields.Many2one('res.users', 'Salesperson', index=True) team_id = fields.Many2one('crm.team', 'Sales Channel', oldname='section_id', index=True) @api.onchange('action') def onchange_action(self): if self.action == 'exist': self.partner_id = self._find_matching_partner() else: self.partner_id = False @api.onchange('user_id') def _onchange_user(self): """ When changing the user, also set a team_id or restrict team id to the ones user_id is member of. """ if self.user_id: if self.team_id: user_in_team = self.env['crm.team'].search_count([ ('id', '=', self.team_id.id), '|', ('user_id', '=', self.user_id.id), ('member_ids', '=', self.user_id.id) ]) else: user_in_team = False if not user_in_team: values = self.env['crm.lead']._onchange_user_values( self.user_id.id if self.user_id else False) self.team_id = values.get('team_id', False) @api.model def _get_duplicated_leads(self, partner_id, email, include_lost=False): """ Search for opportunities that have the same partner and that arent done or cancelled """ return self.env['crm.lead']._get_duplicated_leads_by_emails( partner_id, email, include_lost=include_lost) # NOTE JEM : is it the good place to test this ? @api.model def view_init(self, fields): """ Check some preconditions before the wizard executes. """ for lead in self.env['crm.lead'].browse( self._context.get('active_ids', [])): if lead.probability == 100: raise UserError( _("Closed/Dead leads cannot be converted into opportunities." )) return False @api.multi def _convert_opportunity(self, vals): self.ensure_one() res = False leads = self.env['crm.lead'].browse(vals.get('lead_ids')) for lead in leads: self_def_user = self.with_context(default_user_id=self.user_id.id) partner_id = self_def_user._create_partner( lead.id, self.action, vals.get('partner_id') or lead.partner_id.id) res = lead.convert_opportunity(partner_id, [], False) user_ids = vals.get('user_ids') leads_to_allocate = leads if self._context.get('no_force_assignation'): leads_to_allocate = leads_to_allocate.filtered( lambda lead: not lead.user_id) if user_ids: leads_to_allocate.allocate_salesman(user_ids, team_id=(vals.get('team_id'))) return res @api.multi def action_apply(self): """ Convert lead to opportunity or merge lead and opportunity and open the freshly created opportunity view. """ self.ensure_one() values = { 'team_id': self.team_id.id, } if self.partner_id: values['partner_id'] = self.partner_id.id if self.name == 'merge': leads = self.with_context( active_test=False).opportunity_ids.merge_opportunity() if not leads.active: leads.write({ 'active': True, 'activity_type_id': False, 'lost_reason': False }) if leads.type == "lead": values.update({ 'lead_ids': leads.ids, 'user_ids': [self.user_id.id] }) self.with_context( active_ids=leads.ids)._convert_opportunity(values) elif not self._context.get( 'no_force_assignation') or not leads.user_id: values['user_id'] = self.user_id.id leads.write(values) else: leads = self.env['crm.lead'].browse( self._context.get('active_ids', [])) values.update({ 'lead_ids': leads.ids, 'user_ids': [self.user_id.id] }) self._convert_opportunity(values) return leads[0].redirect_opportunity_view() def _create_partner(self, lead_id, action, partner_id): """ Create partner based on action. :return dict: dictionary organized as followed: {lead_id: partner_assigned_id} """ #TODO this method in only called by Lead2OpportunityPartner #wizard and would probably diserve to be refactored or at least #moved to a better place if action == 'each_exist_or_create': partner_id = self.with_context( active_id=lead_id)._find_matching_partner() action = 'create' result = self.env['crm.lead'].browse( lead_id).handle_partner_assignation(action, partner_id) return result.get(lead_id)
class PurchaseRequisitionLine(models.Model): _name = "purchase.requisition.line" _description = "Purchase Requisition Line" _rec_name = 'product_id' product_id = fields.Many2one('product.product', string='Product', domain=[('purchase_ok', '=', True)], required=True) product_uom_id = fields.Many2one('product.uom', string='Product Unit of Measure') product_qty = fields.Float( string='Quantity', digits=dp.get_precision('Product Unit of Measure')) price_unit = fields.Float(string='Unit Price', digits=dp.get_precision('Product Price')) qty_ordered = fields.Float(compute='_compute_ordered_qty', string='Ordered Quantities') requisition_id = fields.Many2one('purchase.requisition', string='Purchase Agreement', ondelete='cascade') company_id = fields.Many2one( 'res.company', related='requisition_id.company_id', string='Company', store=True, readonly=True, default=lambda self: self.env['res.company']._company_default_get( 'purchase.requisition.line')) account_analytic_id = fields.Many2one('account.analytic.account', string='Analytic Account') schedule_date = fields.Date(string='Scheduled Date') move_dest_id = fields.Many2one('stock.move', 'Downstream Move') @api.multi @api.depends('requisition_id.purchase_ids.state') def _compute_ordered_qty(self): for line in self: total = 0.0 for po in line.requisition_id.purchase_ids.filtered( lambda purchase_order: purchase_order.state in ['purchase', 'done']): for po_line in po.order_line.filtered( lambda order_line: order_line.product_id == line. product_id): if po_line.product_uom != line.product_uom_id: total += po_line.product_uom._compute_quantity( po_line.product_qty, line.product_uom_id) else: total += po_line.product_qty line.qty_ordered = total @api.onchange('product_id') def _onchange_product_id(self): if self.product_id: self.product_uom_id = self.product_id.uom_id self.product_qty = 1.0 if not self.account_analytic_id: self.account_analytic_id = self.requisition_id.account_analytic_id if not self.schedule_date: self.schedule_date = self.requisition_id.schedule_date @api.multi def _prepare_purchase_order_line(self, name, product_qty=0.0, price_unit=0.0, taxes_ids=False): self.ensure_one() requisition = self.requisition_id return { 'name': name, 'product_id': self.product_id.id, 'product_uom': self.product_id.uom_po_id.id, 'product_qty': product_qty, 'price_unit': price_unit, 'taxes_id': [(6, 0, taxes_ids)], 'date_planned': requisition.schedule_date or fields.Date.today(), 'account_analytic_id': self.account_analytic_id.id, 'move_dest_ids': self.move_dest_id and [(4, self.move_dest_id.id)] or [] }
class Picking(models.Model): _name = "stock.picking" _inherit = ['mail.thread', 'mail.activity.mixin'] _description = "Transfer" _order = "priority desc, date asc, id desc" name = fields.Char( 'Reference', default='/', copy=False, index=True, states={'done': [('readonly', True)], 'cancel': [('readonly', True)]}) origin = fields.Char( 'Source Document', index=True, states={'done': [('readonly', True)], 'cancel': [('readonly', True)]}, help="Reference of the document") note = fields.Text('Notes') backorder_id = fields.Many2one( 'stock.picking', 'Back Order of', copy=False, index=True, states={'done': [('readonly', True)], 'cancel': [('readonly', True)]}, help="If this shipment was split, then this field links to the shipment which contains the already processed part.") move_type = fields.Selection([ ('direct', 'As soon as possible'), ('one', 'When all products are ready')], 'Shipping Policy', default='direct', required=True, states={'done': [('readonly', True)], 'cancel': [('readonly', True)]}, help="It specifies goods to be deliver partially or all at once") state = fields.Selection([ ('draft', 'Draft'), ('waiting', 'Waiting Another Operation'), ('confirmed', 'Waiting'), ('assigned', 'Ready'), ('done', 'Done'), ('cancel', 'Cancelled'), ], string='Status', compute='_compute_state', copy=False, index=True, readonly=True, store=True, track_visibility='onchange', help=" * Draft: not confirmed yet and will not be scheduled until confirmed.\n" " * Waiting Another Operation: waiting for another move to proceed before it becomes automatically available (e.g. in Make-To-Order flows).\n" " * Waiting: if it is not ready to be sent because the required products could not be reserved.\n" " * Ready: products are reserved and ready to be sent. If the shipping policy is 'As soon as possible' this happens as soon as anything is reserved.\n" " * Done: has been processed, can't be modified or cancelled anymore.\n" " * Cancelled: has been cancelled, can't be confirmed anymore.") group_id = fields.Many2one( 'procurement.group', 'Procurement Group', readonly=True, related='move_lines.group_id', store=True) priority = fields.Selection( PROCUREMENT_PRIORITIES, string='Priority', compute='_compute_priority', inverse='_set_priority', store=True, # default='1', required=True, # TDE: required, depending on moves ? strange index=True, track_visibility='onchange', states={'done': [('readonly', True)], 'cancel': [('readonly', True)]}, help="Priority for this picking. Setting manually a value here would set it as priority for all the moves") scheduled_date = fields.Datetime( 'Scheduled Date', compute='_compute_scheduled_date', inverse='_set_scheduled_date', store=True, index=True, track_visibility='onchange', states={'done': [('readonly', True)], 'cancel': [('readonly', True)]}, help="Scheduled time for the first part of the shipment to be processed. Setting manually a value here would set it as expected date for all the stock moves.") date = fields.Datetime( 'Creation Date', default=fields.Datetime.now, index=True, track_visibility='onchange', states={'done': [('readonly', True)], 'cancel': [('readonly', True)]}, help="Creation Date, usually the time of the order") date_done = fields.Datetime('Date of Transfer', copy=False, readonly=True, help="Completion Date of Transfer") location_id = fields.Many2one( 'stock.location', "Source Location", default=lambda self: self.env['stock.picking.type'].browse(self._context.get('default_picking_type_id')).default_location_src_id, readonly=True, required=True, states={'draft': [('readonly', False)]}) location_dest_id = fields.Many2one( 'stock.location', "Destination Location", default=lambda self: self.env['stock.picking.type'].browse(self._context.get('default_picking_type_id')).default_location_dest_id, readonly=True, required=True, states={'draft': [('readonly', False)]}) move_lines = fields.One2many('stock.move', 'picking_id', string="Stock Moves", copy=True) has_scrap_move = fields.Boolean( 'Has Scrap Moves', compute='_has_scrap_move') picking_type_id = fields.Many2one( 'stock.picking.type', 'Operation Type', required=True, states={'done': [('readonly', True)], 'cancel': [('readonly', True)]}) picking_type_code = fields.Selection([ ('incoming', 'Vendors'), ('outgoing', 'Customers'), ('internal', 'Internal')], related='picking_type_id.code', readonly=True) picking_type_entire_packs = fields.Boolean(related='picking_type_id.show_entire_packs', readonly=True) partner_id = fields.Many2one( 'res.partner', 'Partner', states={'done': [('readonly', True)], 'cancel': [('readonly', True)]}) company_id = fields.Many2one( 'res.company', 'Company', default=lambda self: self.env['res.company']._company_default_get('stock.picking'), index=True, required=True, states={'done': [('readonly', True)], 'cancel': [('readonly', True)]}) branch_id = fields.Many2one('res.branch', 'Branch', ondelete="restrict", default=lambda self: self.env['res.users']._get_default_branch(), states={'done': [('readonly', True)], 'cancel': [('readonly', True)]}) move_line_ids = fields.One2many('stock.move.line', 'picking_id', 'Operations') move_line_exist = fields.Boolean( 'Has Pack Operations', compute='_compute_move_line_exist', help='Check the existence of pack operation on the picking') has_packages = fields.Boolean( 'Has Packages', compute='_compute_has_packages', help='Check the existence of destination packages on move lines') entire_package_ids = fields.One2many('stock.quant.package', compute='_compute_entire_package_ids', help='Those are the entire packages of a picking shown in the view of operations') entire_package_detail_ids = fields.One2many('stock.quant.package', compute='_compute_entire_package_ids', help='Those are the entire packages of a picking shown in the view of detailed operations') show_check_availability = fields.Boolean( compute='_compute_show_check_availability', help='Technical field used to compute whether the check availability button should be shown.') show_mark_as_todo = fields.Boolean( compute='_compute_show_mark_as_todo', help='Technical field used to compute whether the mark as todo button should be shown.') show_validate = fields.Boolean( compute='_compute_show_validate', help='Technical field used to compute whether the validate should be shown.') owner_id = fields.Many2one( 'res.partner', 'Owner', states={'done': [('readonly', True)], 'cancel': [('readonly', True)]}, help="Default Owner") printed = fields.Boolean('Printed') is_locked = fields.Boolean(default=True, help='When the picking is not done this allows changing the ' 'initial demand. When the picking is done this allows ' 'changing the done quantities.') # Used to search on pickings product_id = fields.Many2one('product.product', 'Product', related='move_lines.product_id') show_operations = fields.Boolean(compute='_compute_show_operations') show_lots_text = fields.Boolean(compute='_compute_show_lots_text') has_tracking = fields.Boolean(compute='_compute_has_tracking') _sql_constraints = [ ('name_uniq', 'unique(name, company_id)', 'Reference must be unique per company!'), ] def _compute_has_tracking(self): for picking in self: picking.has_tracking = any(m.has_tracking != 'none' for m in picking.move_lines) @api.depends('picking_type_id.show_operations') def _compute_show_operations(self): for picking in self: if self.env.context.get('force_detailed_view'): picking.show_operations = True break if picking.picking_type_id.show_operations: if (picking.state == 'draft' and not self.env.context.get('planned_picking')) or picking.state != 'draft': picking.show_operations = True else: picking.show_operations = False else: picking.show_operations = False @api.depends('move_line_ids', 'picking_type_id.use_create_lots', 'picking_type_id.use_existing_lots', 'state') def _compute_show_lots_text(self): group_production_lot_enabled = self.user_has_groups('stock.group_production_lot') for picking in self: if not picking.move_line_ids: picking.show_lots_text = False elif group_production_lot_enabled and picking.picking_type_id.use_create_lots \ and not picking.picking_type_id.use_existing_lots and picking.state != 'done': picking.show_lots_text = True else: picking.show_lots_text = False @api.depends('move_type', 'move_lines.state', 'move_lines.picking_id') @api.one def _compute_state(self): ''' State of a picking depends on the state of its related stock.move - Draft: only used for "planned pickings" - Waiting: if the picking is not ready to be sent so if - (a) no quantity could be reserved at all or if - (b) some quantities could be reserved and the shipping policy is "deliver all at once" - Waiting another move: if the picking is waiting for another move - Ready: if the picking is ready to be sent so if: - (a) all quantities are reserved or if - (b) some quantities could be reserved and the shipping policy is "as soon as possible" - Done: if the picking is done. - Cancelled: if the picking is cancelled ''' if not self.move_lines: self.state = 'draft' elif any(move.state == 'draft' for move in self.move_lines): # TDE FIXME: should be all ? self.state = 'draft' elif all(move.state == 'cancel' for move in self.move_lines): self.state = 'cancel' elif all(move.state in ['cancel', 'done'] for move in self.move_lines): self.state = 'done' else: relevant_move_state = self.move_lines._get_relevant_state_among_moves() if relevant_move_state == 'partially_available': self.state = 'assigned' else: self.state = relevant_move_state @api.one @api.depends('move_lines.priority') def _compute_priority(self): if self.mapped('move_lines'): priorities = [priority for priority in self.mapped('move_lines.priority') if priority] or ['1'] self.priority = max(priorities) else: self.priority = '1' @api.one def _set_priority(self): self.move_lines.write({'priority': self.priority}) @api.one @api.depends('move_lines.date_expected') def _compute_scheduled_date(self): if self.move_type == 'direct': self.scheduled_date = min(self.move_lines.mapped('date_expected') or [fields.Datetime.now()]) else: self.scheduled_date = max(self.move_lines.mapped('date_expected') or [fields.Datetime.now()]) @api.one def _set_scheduled_date(self): self.move_lines.write({'date_expected': self.scheduled_date}) @api.one def _has_scrap_move(self): # TDE FIXME: better implementation self.has_scrap_move = bool(self.env['stock.move'].search_count([('picking_id', '=', self.id), ('scrapped', '=', True)])) @api.one def _compute_move_line_exist(self): self.move_line_exist = bool(self.move_line_ids) @api.one def _compute_has_packages(self): self.has_packages = self.move_line_ids.filtered(lambda ml: ml.result_package_id) def _compute_entire_package_ids(self): """ This compute method populate the two one2Many containing all entire packages of the picking. An entire package is a package that is entirely reserved to be moved from a location to another one. """ for picking in self: packages = self.env['stock.quant.package'] packages_to_check = picking.move_line_ids\ .filtered(lambda ml: ml.result_package_id and ml.package_id.id == ml.result_package_id.id)\ .mapped('package_id') for package_to_check in packages_to_check: if picking.state in ('done', 'cancel') or picking._check_move_lines_map_quant_package(package_to_check): packages |= package_to_check picking.entire_package_ids = packages picking.entire_package_detail_ids = packages @api.multi def _compute_show_check_availability(self): for picking in self: has_moves_to_reserve = any( move.state in ('waiting', 'confirmed', 'partially_available') and float_compare(move.product_uom_qty, 0, precision_rounding=move.product_uom.rounding) for move in picking.move_lines ) picking.show_check_availability = picking.is_locked and picking.state in ('confirmed', 'waiting', 'assigned') and has_moves_to_reserve @api.multi @api.depends('state', 'move_lines') def _compute_show_mark_as_todo(self): for picking in self: if not picking.move_lines: picking.show_mark_as_todo = False elif self._context.get('planned_picking') and picking.state == 'draft': picking.show_mark_as_todo = True elif picking.state != 'draft' or not picking.id: picking.show_mark_as_todo = False else: picking.show_mark_as_todo = True @api.multi @api.depends('state', 'is_locked') def _compute_show_validate(self): for picking in self: if self._context.get('planned_picking') and picking.state == 'draft': picking.show_validate = False elif picking.state not in ('draft', 'waiting', 'confirmed', 'assigned') or not picking.is_locked: picking.show_validate = False else: picking.show_validate = True @api.onchange('picking_type_id', 'partner_id') def onchange_picking_type(self): if self.picking_type_id: if self.picking_type_id.default_location_src_id: location_id = self.picking_type_id.default_location_src_id.id elif self.partner_id: location_id = self.partner_id.property_stock_supplier.id else: customerloc, location_id = self.env['stock.warehouse']._get_partner_locations() if self.picking_type_id.default_location_dest_id: location_dest_id = self.picking_type_id.default_location_dest_id.id elif self.partner_id: location_dest_id = self.partner_id.property_stock_customer.id else: location_dest_id, supplierloc = self.env['stock.warehouse']._get_partner_locations() self.location_id = location_id self.location_dest_id = location_dest_id # TDE CLEANME move into onchange_partner_id if self.partner_id: if self.partner_id.picking_warn == 'no-message' and self.partner_id.parent_id: partner = self.partner_id.parent_id elif self.partner_id.picking_warn not in ('no-message', 'block') and self.partner_id.parent_id.picking_warn == 'block': partner = self.partner_id.parent_id else: partner = self.partner_id if partner.picking_warn != 'no-message': if partner.picking_warn == 'block': self.partner_id = False return {'warning': { 'title': ("Warning for %s") % partner.name, 'message': partner.picking_warn_msg }} @api.model def create(self, vals): # TDE FIXME: clean that brol defaults = self.default_get(['name', 'picking_type_id']) if vals.get('name', '/') == '/' and defaults.get('name', '/') == '/' and vals.get('picking_type_id', defaults.get('picking_type_id')): vals['name'] = self.env['stock.picking.type'].browse(vals.get('picking_type_id', defaults.get('picking_type_id'))).sequence_id.next_by_id() # TDE FIXME: what ? # As the on_change in one2many list is WIP, we will overwrite the locations on the stock moves here # As it is a create the format will be a list of (0, 0, dict) if vals.get('move_lines') and vals.get('location_id') and vals.get('location_dest_id'): for move in vals['move_lines']: if len(move) == 3: move[2]['location_id'] = vals['location_id'] move[2]['location_dest_id'] = vals['location_dest_id'] res = super(Picking, self).create(vals) res._autoconfirm_picking() return res @api.multi def write(self, vals): res = super(Picking, self).write(vals) # Change locations of moves if those of the picking change after_vals = {} if vals.get('location_id'): after_vals['location_id'] = vals['location_id'] if vals.get('location_dest_id'): after_vals['location_dest_id'] = vals['location_dest_id'] if after_vals: self.mapped('move_lines').filtered(lambda move: not move.scrapped).write(after_vals) if vals.get('move_lines'): # Do not run autoconfirm if any of the moves has an initial demand. If an initial demand # is present in any of the moves, it means the picking was created through the "planned # transfer" mechanism. pickings_to_not_autoconfirm = self.env['stock.picking'] for picking in self: if picking.state != 'draft': continue for move in picking.move_lines: if not float_is_zero(move.product_uom_qty, precision_rounding=move.product_uom.rounding): pickings_to_not_autoconfirm |= picking break (self - pickings_to_not_autoconfirm)._autoconfirm_picking() return res @api.multi def unlink(self): self.mapped('move_lines')._action_cancel() self.mapped('move_lines').unlink() # Checks if moves are not done return super(Picking, self).unlink() # Actions # ---------------------------------------- @api.one def action_assign_owner(self): self.move_line_ids.write({'owner_id': self.owner_id.id}) @api.multi def do_print_picking(self): self.write({'printed': True}) return self.env.ref('stock.action_report_picking').report_action(self) @api.multi def action_confirm(self): # call `_action_confirm` on every draft move self.mapped('move_lines')\ .filtered(lambda move: move.state == 'draft')\ ._action_confirm() # call `_action_assign` on every confirmed move which location_id bypasses the reservation self.filtered(lambda picking: picking.location_id.usage in ('supplier', 'inventory', 'production') and picking.state == 'confirmed')\ .mapped('move_lines')._action_assign() if self.env.context.get('planned_picking') and len(self) == 1: action = self.env.ref('stock.action_picking_form') result = action.read()[0] result['res_id'] = self.id result['context'] = { 'search_default_picking_type_id': [self.picking_type_id.id], 'default_picking_type_id': self.picking_type_id.id, 'contact_display': 'partner_address', 'planned_picking': False, } return result else: return True @api.multi def action_assign(self): """ Check availability of picking moves. This has the effect of changing the state and reserve quants on available moves, and may also impact the state of the picking as it is computed based on move's states. @return: True """ self.filtered(lambda picking: picking.state == 'draft').action_confirm() moves = self.mapped('move_lines').filtered(lambda move: move.state not in ('draft', 'cancel', 'done')) if not moves: raise UserError(_('Nothing to check the availability for.')) moves._action_assign() return True @api.multi def force_assign(self): """ Changes state of picking to available if moves are confirmed or waiting. @return: True """ self.mapped('move_lines').filtered(lambda move: move.state in ['confirmed', 'waiting', 'partially_available'])._force_assign() return True @api.multi def action_cancel(self): self.mapped('move_lines')._action_cancel() self.write({'is_locked': True}) return True @api.multi def action_done(self): """Changes picking state to done by processing the Stock Moves of the Picking Normally that happens when the button "Done" is pressed on a Picking view. @return: True """ # TDE FIXME: remove decorator when migration the remaining todo_moves = self.mapped('move_lines').filtered(lambda self: self.state in ['draft', 'waiting', 'partially_available', 'assigned', 'confirmed']) # Check if there are ops not linked to moves yet for pick in self: # # Explode manually added packages # for ops in pick.move_line_ids.filtered(lambda x: not x.move_id and not x.product_id): # for quant in ops.package_id.quant_ids: #Or use get_content for multiple levels # self.move_line_ids.create({'product_id': quant.product_id.id, # 'package_id': quant.package_id.id, # 'result_package_id': ops.result_package_id, # 'lot_id': quant.lot_id.id, # 'owner_id': quant.owner_id.id, # 'product_uom_id': quant.product_id.uom_id.id, # 'product_qty': quant.qty, # 'qty_done': quant.qty, # 'location_id': quant.location_id.id, # Could be ops too # 'location_dest_id': ops.location_dest_id.id, # 'picking_id': pick.id # }) # Might change first element # # Link existing moves or add moves when no one is related for ops in pick.move_line_ids.filtered(lambda x: not x.move_id): # Search move with this product moves = pick.move_lines.filtered(lambda x: x.product_id == ops.product_id) if moves: #could search move that needs it the most (that has some quantities left) ops.move_id = moves[0].id else: new_move = self.env['stock.move'].create({ 'name': _('New Move:') + ops.product_id.display_name, 'product_id': ops.product_id.id, 'product_uom_qty': ops.qty_done, 'product_uom': ops.product_uom_id.id, 'location_id': pick.location_id.id, 'location_dest_id': pick.location_dest_id.id, 'picking_id': pick.id, }) ops.move_id = new_move.id new_move._action_confirm() todo_moves |= new_move #'qty_done': ops.qty_done}) todo_moves._action_done() self.write({'date_done': fields.Datetime.now()}) return True # Backward compatibility # Problem with fixed reference to a function: # it doesn't allow for overriding action_done() through do_transfer # get rid of me in master (and make me private ?) def do_transfer(self): return self.action_done() def _check_move_lines_map_quant_package(self, package): """ This method checks that all product of the package (quant) are well present in the move_line_ids of the picking. """ all_in = True pack_move_lines = self.move_line_ids.filtered(lambda ml: ml.package_id == package) keys = ['product_id', 'lot_id'] grouped_quants = {} for k, g in groupby(sorted(package.quant_ids, key=itemgetter(*keys)), key=itemgetter(*keys)): grouped_quants[k] = sum(self.env['stock.quant'].concat(*list(g)).mapped('quantity')) grouped_ops = {} for k, g in groupby(sorted(pack_move_lines, key=itemgetter(*keys)), key=itemgetter(*keys)): grouped_ops[k] = sum(self.env['stock.move.line'].concat(*list(g)).mapped('product_qty')) if any(grouped_quants.get(key, 0) - grouped_ops.get(key, 0) != 0 for key in grouped_quants) \ or any(grouped_ops.get(key, 0) - grouped_quants.get(key, 0) != 0 for key in grouped_ops): all_in = False return all_in @api.multi def _check_entire_pack(self): """ This function check if entire packs are moved in the picking""" for picking in self: origin_packages = picking.move_line_ids.mapped("package_id") for pack in origin_packages: if picking._check_move_lines_map_quant_package(pack): picking.move_line_ids.filtered(lambda ml: ml.package_id == pack).write({'result_package_id': pack.id}) @api.multi def do_unreserve(self): for picking in self: picking.move_lines._do_unreserve() @api.multi def button_validate(self): self.ensure_one() if not self.move_lines and not self.move_line_ids: raise UserError(_('Please add some lines to move')) # If no lots when needed, raise error picking_type = self.picking_type_id no_quantities_done = all(float_is_zero(move_line.qty_done, precision_rounding=move_line.product_uom_id.rounding) for move_line in self.move_line_ids) no_reserved_quantities = all(float_is_zero(move_line.product_qty, precision_rounding=move_line.product_uom_id.rounding) for move_line in self.move_line_ids) if no_reserved_quantities and no_quantities_done: raise UserError(_('You cannot validate a transfer if you have not processed any quantity. You should rather cancel the transfer.')) if picking_type.use_create_lots or picking_type.use_existing_lots: lines_to_check = self.move_line_ids if not no_quantities_done: lines_to_check = lines_to_check.filtered( lambda line: float_compare(line.qty_done, 0, precision_rounding=line.product_uom_id.rounding) ) for line in lines_to_check: product = line.product_id if product and product.tracking != 'none': if not line.lot_name and not line.lot_id: raise UserError(_('You need to supply a lot/serial number for %s.') % product.display_name) elif line.qty_done == 0: raise UserError(_('You cannot validate a transfer if you have not processed any quantity for %s.') % product.display_name) if no_quantities_done: view = self.env.ref('stock.view_immediate_transfer') wiz = self.env['stock.immediate.transfer'].create({'pick_ids': [(4, self.id)]}) return { 'name': _('Immediate Transfer?'), 'type': 'ir.actions.act_window', 'view_type': 'form', 'view_mode': 'form', 'res_model': 'stock.immediate.transfer', 'views': [(view.id, 'form')], 'view_id': view.id, 'target': 'new', 'res_id': wiz.id, 'context': self.env.context, } if self._get_overprocessed_stock_moves() and not self._context.get('skip_overprocessed_check'): view = self.env.ref('stock.view_overprocessed_transfer') wiz = self.env['stock.overprocessed.transfer'].create({'picking_id': self.id}) return { 'type': 'ir.actions.act_window', 'view_type': 'form', 'view_mode': 'form', 'res_model': 'stock.overprocessed.transfer', 'views': [(view.id, 'form')], 'view_id': view.id, 'target': 'new', 'res_id': wiz.id, 'context': self.env.context, } # Check backorder should check for other barcodes if self._check_backorder(): return self.action_generate_backorder_wizard() self.action_done() return def action_generate_backorder_wizard(self): view = self.env.ref('stock.view_backorder_confirmation') wiz = self.env['stock.backorder.confirmation'].create({'pick_ids': [(4, p.id) for p in self]}) return { 'name': _('Create Backorder?'), 'type': 'ir.actions.act_window', 'view_type': 'form', 'view_mode': 'form', 'res_model': 'stock.backorder.confirmation', 'views': [(view.id, 'form')], 'view_id': view.id, 'target': 'new', 'res_id': wiz.id, 'context': self.env.context, } def action_toggle_is_locked(self): self.ensure_one() self.is_locked = not self.is_locked return True def _check_backorder(self): """ This method will loop over all the move lines of self and check if creating a backorder is necessary. This method is called during button_validate if the user has already processed some quantities and in the immediate transfer wizard that is displayed if the user has not processed any quantities. :return: True if a backorder is necessary else False """ quantity_todo = {} quantity_done = {} for move in self.mapped('move_lines'): quantity_todo.setdefault(move.product_id.id, 0) quantity_done.setdefault(move.product_id.id, 0) quantity_todo[move.product_id.id] += move.product_uom_qty quantity_done[move.product_id.id] += move.quantity_done for ops in self.mapped('move_line_ids').filtered(lambda x: x.package_id and not x.product_id and not x.move_id): for quant in ops.package_id.quant_ids: quantity_done.setdefault(quant.product_id.id, 0) quantity_done[quant.product_id.id] += quant.qty for pack in self.mapped('move_line_ids').filtered(lambda x: x.product_id and not x.move_id): quantity_done.setdefault(pack.product_id.id, 0) quantity_done[pack.product_id.id] += pack.qty_done return any(quantity_done[x] < quantity_todo.get(x, 0) for x in quantity_done) @api.multi def _autoconfirm_picking(self): if not self._context.get('planned_picking'): for picking in self.filtered(lambda picking: picking.state not in ('done', 'cancel') and picking.move_lines): picking.action_confirm() def _get_overprocessed_stock_moves(self): self.ensure_one() return self.move_lines.filtered( lambda move: move.product_uom_qty != 0 and float_compare(move.quantity_done, move.product_uom_qty, precision_rounding=move.product_uom.rounding) == 1 ) @api.multi def _create_backorder(self, backorder_moves=[]): """ Move all non-done lines into a new backorder picking. """ backorders = self.env['stock.picking'] for picking in self: moves_to_backorder = picking.move_lines.filtered(lambda x: x.state not in ('done', 'cancel')) if moves_to_backorder: backorder_picking = picking.copy({ 'name': '/', 'move_lines': [], 'move_line_ids': [], 'backorder_id': picking.id }) picking.message_post( _('The backorder <a href=# data-oe-model=stock.picking data-oe-id=%d>%s</a> has been created.') % ( backorder_picking.id, backorder_picking.name)) moves_to_backorder.write({'picking_id': backorder_picking.id}) moves_to_backorder.mapped('move_line_ids').write({'picking_id': backorder_picking.id}) backorder_picking.action_assign() backorders |= backorder_picking return backorders def _put_in_pack(self): package = False for pick in self.filtered(lambda p: p.state not in ('done', 'cancel')): operations = pick.move_line_ids.filtered(lambda o: o.qty_done > 0 and not o.result_package_id) operation_ids = self.env['stock.move.line'] if operations: package = self.env['stock.quant.package'].create({}) for operation in operations: if float_compare(operation.qty_done, operation.product_uom_qty, precision_rounding=operation.product_uom_id.rounding) >= 0: operation_ids |= operation else: quantity_left_todo = float_round( operation.product_uom_qty - operation.qty_done, precision_rounding=operation.product_uom_id.rounding, rounding_method='UP') done_to_keep = operation.qty_done new_operation = operation.copy( default={'product_uom_qty': 0, 'qty_done': operation.qty_done}) operation.write({'product_uom_qty': quantity_left_todo, 'qty_done': 0.0}) new_operation.write({'product_uom_qty': done_to_keep}) operation_ids |= new_operation operation_ids.write({'result_package_id': package.id}) else: raise UserError(_('Please process some quantities to put in the pack first!')) return package def put_in_pack(self): return self._put_in_pack() def button_scrap(self): self.ensure_one() products = self.env['product.product'] for move in self.move_lines: if move.state not in ('draft', 'cancel') and move.product_id.type in ('product', 'consu'): products |= move.product_id 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_picking_id': self.id, 'product_ids': products.ids}, 'target': 'new', } def action_see_move_scrap(self): self.ensure_one() action = self.env.ref('stock.action_stock_scrap').read()[0] scraps = self.env['stock.scrap'].search([('picking_id', '=', self.id)]) action['domain'] = [('id', 'in', scraps.ids)] return action def action_see_packages(self): self.ensure_one() action = self.env.ref('stock.action_package_view').read()[0] packages = self.move_line_ids.mapped('result_package_id') action['domain'] = [('id', 'in', packages.ids)] action['context'] = {'picking_id': self.id} return action
class PurchaseOrder(models.Model): _inherit = "purchase.order" requisition_id = fields.Many2one('purchase.requisition', string='Purchase Agreement', copy=False) @api.onchange('requisition_id') def _onchange_requisition_id(self): if not self.requisition_id: return requisition = self.requisition_id if self.partner_id: partner = self.partner_id else: partner = requisition.vendor_id payment_term = partner.property_supplier_payment_term_id currency = partner.property_purchase_currency_id or requisition.company_id.currency_id FiscalPosition = self.env['account.fiscal.position'] fpos = FiscalPosition.get_fiscal_position(partner.id) fpos = FiscalPosition.browse(fpos) self.partner_id = partner.id self.fiscal_position_id = fpos.id self.payment_term_id = payment_term.id, self.company_id = requisition.company_id.id self.currency_id = currency.id self.origin = requisition.name self.partner_ref = requisition.name # to control vendor bill based on agreement reference self.notes = requisition.description self.date_order = requisition.date_end or fields.Datetime.now() self.picking_type_id = requisition.picking_type_id.id if requisition.type_id.line_copy != 'copy': return # Create PO lines if necessary order_lines = [] for line in requisition.line_ids: # Compute name product_lang = line.product_id.with_context({ 'lang': partner.lang, 'partner_id': partner.id, }) name = product_lang.display_name if product_lang.description_purchase: name += '\n' + product_lang.description_purchase # Compute taxes if fpos: taxes_ids = fpos.map_tax( line.product_id.supplier_taxes_id.filtered( lambda tax: tax.company_id == requisition.company_id) ).ids else: taxes_ids = line.product_id.supplier_taxes_id.filtered( lambda tax: tax.company_id == requisition.company_id).ids # Compute quantity and price_unit if line.product_uom_id != line.product_id.uom_po_id: product_qty = line.product_uom_id._compute_quantity( line.product_qty, line.product_id.uom_po_id) price_unit = line.product_uom_id._compute_price( line.price_unit, line.product_id.uom_po_id) else: product_qty = line.product_qty price_unit = line.price_unit if requisition.type_id.quantity_copy != 'copy': product_qty = 0 # Compute price_unit in appropriate currency if requisition.company_id.currency_id != currency: price_unit = requisition.company_id.currency_id.compute( price_unit, currency) # Create PO line order_line_values = line._prepare_purchase_order_line( name=name, product_qty=product_qty, price_unit=price_unit, taxes_ids=taxes_ids) order_lines.append((0, 0, order_line_values)) self.order_line = order_lines @api.multi def button_confirm(self): res = super(PurchaseOrder, self).button_confirm() for po in self: if not po.requisition_id: continue if po.requisition_id.type_id.exclusive == 'exclusive': others_po = po.requisition_id.mapped('purchase_ids').filtered( lambda r: r.id != po.id) others_po.button_cancel() po.requisition_id.action_done() return res @api.model def create(self, vals): purchase = super(PurchaseOrder, self).create(vals) if purchase.requisition_id: purchase.message_post_with_view( 'mail.message_origin_link', values={ 'self': purchase, 'origin': purchase.requisition_id }, subtype_id=self.env['ir.model.data'].xmlid_to_res_id( 'mail.mt_note')) return purchase @api.multi def write(self, vals): result = super(PurchaseOrder, self).write(vals) if vals.get('requisition_id'): self.message_post_with_view( 'mail.message_origin_link', values={ 'self': self, 'origin': self.requisition_id, 'edit': True }, subtype_id=self.env['ir.model.data'].xmlid_to_res_id( 'mail.mt_note')) return result
class PosConfig(models.Model): _name = 'pos.config' def _default_sale_journal(self): journal = self.env.ref('point_of_sale.pos_sale_journal', raise_if_not_found=False) if journal and journal.sudo().company_id == self.env.user.company_id: return journal return self._default_invoice_journal() def _default_invoice_journal(self): return self.env['account.journal'].search( [('type', '=', 'sale'), ('company_id', '=', self.env.user.company_id.id)], limit=1) def _default_pricelist(self): return self.env['product.pricelist'].search( [('currency_id', '=', self.env.user.company_id.currency_id.id)], limit=1) def _get_default_location(self): return self.env['stock.warehouse'].search( [('company_id', '=', self.env.user.company_id.id)], limit=1).lot_stock_id def _get_group_pos_manager(self): return self.env.ref('point_of_sale.group_pos_manager') def _get_group_pos_user(self): return self.env.ref('point_of_sale.group_pos_user') def _compute_default_customer_html(self): return self.env['ir.qweb'].render( 'point_of_sale.customer_facing_display_html') name = fields.Char(string='Point of Sale Name', index=True, required=True, help="An internal identification of the point of sale.") is_installed_account_accountant = fields.Boolean( compute="_compute_is_installed_account_accountant") journal_ids = fields.Many2many( 'account.journal', 'pos_config_journal_rel', 'pos_config_id', 'journal_id', string='Available Payment Methods', domain= "[('journal_user', '=', True ), ('type', 'in', ['bank', 'cash'])]", ) picking_type_id = fields.Many2one('stock.picking.type', string='Operation Type') use_existing_lots = fields.Boolean( related='picking_type_id.use_existing_lots') stock_location_id = fields.Many2one('stock.location', string='Stock Location', domain=[('usage', '=', 'internal')], required=True, default=_get_default_location) journal_id = fields.Many2one( 'account.journal', string='Sales Journal', domain=[('type', '=', 'sale')], help="Accounting journal used to post sales entries.", default=_default_sale_journal) invoice_journal_id = fields.Many2one( 'account.journal', string='Invoice Journal', domain=[('type', '=', 'sale')], help="Accounting journal used to create invoices.", default=_default_invoice_journal) currency_id = fields.Many2one('res.currency', compute='_compute_currency', string="Currency") iface_cashdrawer = fields.Boolean( string='Cashdrawer', help="Automatically open the cashdrawer.") iface_payment_terminal = fields.Boolean( string='Payment Terminal', help="Enables Payment Terminal integration.") iface_electronic_scale = fields.Boolean( string='Electronic Scale', help="Enables Electronic Scale integration.") iface_vkeyboard = fields.Boolean( string='Virtual KeyBoard', help= u"Don’t turn this option on if you take orders on smartphones or tablets. \n Such devices already benefit from a native keyboard." ) iface_customer_facing_display = fields.Boolean( string='Customer Facing Display', help="Show checkout to customers with a remotely-connected screen.") iface_print_via_proxy = fields.Boolean( string='Print via Proxy', help="Bypass browser printing and prints via the hardware proxy.") iface_scan_via_proxy = fields.Boolean( string='Scan via Proxy', help= "Enable barcode scanning with a remotely connected barcode scanner.") iface_invoicing = fields.Boolean( string='Invoicing', help='Enables invoice generation from the Point of Sale.') iface_big_scrollbars = fields.Boolean( 'Large Scrollbars', help='For imprecise industrial touchscreens.') iface_print_auto = fields.Boolean( string='Automatic Receipt Printing', default=False, help= 'The receipt will automatically be printed at the end of each order.') iface_print_skip_screen = fields.Boolean( string='Skip Preview Screen', default=True, help= 'The receipt screen will be skipped if the receipt can be printed automatically.' ) iface_precompute_cash = fields.Boolean( string='Prefill Cash Payment', help= 'The payment input will behave similarily to bank payment input, and will be prefilled with the exact due amount.' ) iface_tax_included = fields.Selection([('subtotal', 'Tax-Excluded Prices'), ('total', 'Tax-Included Prices')], "Tax Display", default='subtotal', required=True) iface_start_categ_id = fields.Many2one( 'pos.category', string='Initial Category', help= 'The point of sale will display this product category by default. If no category is specified, all available products will be shown.' ) iface_display_categ_images = fields.Boolean( string='Display Category Pictures', help="The product categories will be displayed with pictures.") restrict_price_control = fields.Boolean( string='Restrict Price Modifications to Managers', help= "Only users with Manager access rights for PoS app can modify the product prices on orders." ) cash_control = fields.Boolean( string='Cash Control', help="Check the amount of the cashbox at opening and closing.") receipt_header = fields.Text( string='Receipt Header', help= "A short text that will be inserted as a header in the printed receipt." ) receipt_footer = fields.Text( string='Receipt Footer', help= "A short text that will be inserted as a footer in the printed receipt." ) proxy_ip = fields.Char( string='IP Address', size=45, help= 'The hostname or ip address of the hardware proxy, Will be autodetected if left empty.' ) active = fields.Boolean(default=True) uuid = fields.Char( readonly=True, default=lambda self: str(uuid.uuid4()), help= 'A globally unique identifier for this pos configuration, used to prevent conflicts in client-generated data.' ) sequence_id = fields.Many2one( 'ir.sequence', string='Order IDs Sequence', readonly=True, help= "This sequence is automatically created by actpy but you can change it " "to customize the reference numbers of your orders.", copy=False) sequence_line_id = fields.Many2one( 'ir.sequence', string='Order Line IDs Sequence', readonly=True, help= "This sequence is automatically created by actpy but you can change it " "to customize the reference numbers of your orders lines.", copy=False) session_ids = fields.One2many('pos.session', 'config_id', string='Sessions') current_session_id = fields.Many2one('pos.session', compute='_compute_current_session', string="Current Session") current_session_state = fields.Char(compute='_compute_current_session') last_session_closing_cash = fields.Float(compute='_compute_last_session') last_session_closing_date = fields.Date(compute='_compute_last_session') pos_session_username = fields.Char(compute='_compute_current_session_user') pos_session_state = fields.Char(compute='_compute_current_session_user') group_by = fields.Boolean( string='Group Journal Items', default=True, help= "Check this if you want to group the Journal Items by Product while closing a Session." ) pricelist_id = fields.Many2one( 'product.pricelist', string='Default Pricelist', required=True, default=_default_pricelist, help= "The pricelist used if no customer is selected or if the customer has no Sale Pricelist configured." ) available_pricelist_ids = fields.Many2many( 'product.pricelist', string='Available Pricelists', default=_default_pricelist, help= "Make several pricelists available in the Point of Sale. You can also apply a pricelist to specific customers from their contact form (in Sales tab). To be valid, this pricelist must be listed here as an available pricelist. Otherwise the default pricelist will apply." ) company_id = fields.Many2one('res.company', string='Company', required=True, default=lambda self: self.env.user.company_id) barcode_nomenclature_id = fields.Many2one( 'barcode.nomenclature', string='Barcode Nomenclature', help= 'Defines what kind of barcodes are available and how they are assigned to products, customers and cashiers.' ) group_pos_manager_id = fields.Many2one( 'res.groups', string='Point of Sale Manager Group', default=_get_group_pos_manager, help= 'This field is there to pass the id of the pos manager group to the point of sale client.' ) group_pos_user_id = fields.Many2one( 'res.groups', string='Point of Sale User Group', default=_get_group_pos_user, help= 'This field is there to pass the id of the pos user group to the point of sale client.' ) iface_tipproduct = fields.Boolean(string="Product tips") tip_product_id = fields.Many2one( 'product.product', string='Tip Product', help="This product is used as reference on customer receipts.") fiscal_position_ids = fields.Many2many( 'account.fiscal.position', string='Fiscal Positions', help= 'This is useful for restaurants with onsite and take-away services that imply specific tax rates.' ) default_fiscal_position_id = fields.Many2one( 'account.fiscal.position', string='Default Fiscal Position') default_cashbox_lines_ids = fields.One2many('account.cashbox.line', 'default_pos_id', string='Default Balance') customer_facing_display_html = fields.Html( string='Customer facing display content', translate=True, default=_compute_default_customer_html) use_pricelist = fields.Boolean("Use a pricelist.") group_sale_pricelist = fields.Boolean( "Use pricelists to adapt your price per customers", implied_group='product.group_sale_pricelist', help= """Allows to manage different prices based on rules per category of customers. Example: 10% for retailers, promotion of 5 EUR on this product, etc.""" ) group_pricelist_item = fields.Boolean( "Show pricelists to customers", implied_group='product.group_pricelist_item') tax_regime = fields.Boolean("Tax Regime") tax_regime_selection = fields.Boolean("Tax Regime Selection value") barcode_scanner = fields.Boolean("Barcode Scanner") start_category = fields.Boolean("Set Start Category") module_pos_restaurant = fields.Boolean("Is a Bar/Restaurant") module_pos_discount = fields.Boolean("Global Discounts") module_pos_mercury = fields.Boolean(string="Integrated Card Payments") module_pos_reprint = fields.Boolean(string="Reprint Receipt") is_posbox = fields.Boolean("PosBox") is_header_or_footer = fields.Boolean("Header & Footer") def _compute_is_installed_account_accountant(self): account_accountant = self.env['ir.module.module'].sudo().search([ ('name', '=', 'account_accountant'), ('state', '=', 'installed') ]) for pos_config in self: pos_config.is_installed_account_accountant = account_accountant and account_accountant.id @api.depends('journal_id.currency_id', 'journal_id.company_id.currency_id') def _compute_currency(self): for pos_config in self: if pos_config.journal_id: pos_config.currency_id = pos_config.journal_id.currency_id.id or pos_config.journal_id.company_id.currency_id.id else: pos_config.currency_id = self.env.user.company_id.currency_id.id @api.depends('session_ids') def _compute_current_session(self): for pos_config in self: session = pos_config.session_ids.filtered(lambda r: r.user_id.id == self.env.uid and \ not r.state == 'closed' and \ not r.rescue) # sessions ordered by id desc pos_config.current_session_id = session and session[0].id or False pos_config.current_session_state = session and session[ 0].state or False @api.depends('session_ids') def _compute_last_session(self): PosSession = self.env['pos.session'] for pos_config in self: session = PosSession.search_read( [('config_id', '=', pos_config.id), ('state', '=', 'closed')], ['cash_register_balance_end_real', 'stop_at'], order="stop_at desc", limit=1) if session: pos_config.last_session_closing_cash = session[0][ 'cash_register_balance_end_real'] pos_config.last_session_closing_date = session[0]['stop_at'] else: pos_config.last_session_closing_cash = 0 pos_config.last_session_closing_date = False @api.depends('session_ids') def _compute_current_session_user(self): for pos_config in self: session = pos_config.session_ids.filtered(lambda s: s.state in [ 'opening_control', 'opened', 'closing_control' ] and not s.rescue) pos_config.pos_session_username = session and session[ 0].user_id.name or False pos_config.pos_session_state = session and session[0].state or False @api.constrains('company_id', 'stock_location_id') def _check_company_location(self): if self.stock_location_id.company_id and self.stock_location_id.company_id.id != self.company_id.id: raise ValidationError( _("The company of the stock location is different than the one of point of sale" )) @api.constrains('company_id', 'journal_id') def _check_company_journal(self): if self.journal_id and self.journal_id.company_id.id != self.company_id.id: raise ValidationError( _("The company of the sales journal is different than the one of point of sale" )) @api.constrains('company_id', 'invoice_journal_id') def _check_company_invoice_journal(self): if self.invoice_journal_id and self.invoice_journal_id.company_id.id != self.company_id.id: raise ValidationError( _("The invoice journal and the point of sale must belong to the same company" )) @api.constrains('company_id', 'journal_ids') def _check_company_payment(self): if self.env['account.journal'].search_count([ ('id', 'in', self.journal_ids.ids), ('company_id', '!=', self.company_id.id) ]): raise ValidationError( _("The company of a payment method is different than the one of point of sale" )) @api.constrains('pricelist_id', 'available_pricelist_ids', 'journal_id', 'invoice_journal_id', 'journal_ids') def _check_currencies(self): if self.pricelist_id not in self.available_pricelist_ids: raise ValidationError( _("The default pricelist must be included in the available pricelists." )) if any( self.available_pricelist_ids.mapped( lambda pricelist: pricelist.currency_id != self.currency_id )): raise ValidationError( _("All available pricelists must be in the same currency as the company or" " as the Sales Journal set on this point of sale if you use" " the Accounting application.")) if self.invoice_journal_id.currency_id and self.invoice_journal_id.currency_id != self.currency_id: raise ValidationError( _("The invoice journal must be in the same currency as the Sales Journal or the company currency if that is not set." )) if any( self.journal_ids.mapped( lambda journal: journal.currency_id and journal.currency_id != self.currency_id)): raise ValidationError( _("All payment methods must be in the same currency as the Sales Journal or the company currency if that is not set." )) @api.onchange('iface_print_via_proxy') def _onchange_iface_print_via_proxy(self): self.iface_print_auto = self.iface_print_via_proxy @api.onchange('picking_type_id') def _onchange_picking_type_id(self): if self.picking_type_id.default_location_src_id.usage == 'internal' and self.picking_type_id.default_location_dest_id.usage == 'customer': self.stock_location_id = self.picking_type_id.default_location_src_id.id @api.onchange('use_pricelist') def _onchange_use_pricelist(self): """ If the 'pricelist' box is unchecked, we reset the pricelist_id to stop using a pricelist for this posbox. """ if not self.use_pricelist: self.pricelist_id = self._default_pricelist() else: self.update({ 'group_sale_pricelist': True, 'group_pricelist_item': True, }) @api.onchange('available_pricelist_ids') def _onchange_available_pricelist_ids(self): if self.pricelist_id not in self.available_pricelist_ids: self.pricelist_id = False @api.onchange('iface_scan_via_proxy') def _onchange_iface_scan_via_proxy(self): if self.iface_scan_via_proxy: self.barcode_scanner = True else: self.barcode_scanner = False @api.onchange('barcode_scanner') def _onchange_barcode_scanner(self): if self.barcode_scanner: self.barcode_nomenclature_id = self.env[ 'barcode.nomenclature'].search([], limit=1) else: self.barcode_nomenclature_id = False @api.onchange('is_posbox') def _onchange_is_posbox(self): if not self.is_posbox: self.proxy_ip = False self.iface_scan_via_proxy = False self.iface_electronic_scale = False self.iface_cashdrawer = False self.iface_print_via_proxy = False self.iface_customer_facing_display = False @api.onchange('tax_regime') def _onchange_tax_regime(self): if not self.tax_regime: self.default_fiscal_position_id = False @api.onchange('tax_regime_selection') def _onchange_tax_regime_selection(self): if not self.tax_regime_selection: self.fiscal_position_ids = [(5, 0, 0)] @api.onchange('start_category') def _onchange_start_category(self): if not self.start_category: self.iface_start_categ_id = False @api.onchange('is_header_or_footer') def _onchange_header_footer(self): if not self.is_header_or_footer: self.receipt_header = False self.receipt_footer = False @api.multi def name_get(self): result = [] for config in self: if (not config.session_ids) or (config.session_ids[0].state == 'closed'): result.append( (config.id, config.name + ' (' + _('not used') + ')')) continue result.append((config.id, config.name + ' (' + config.session_ids[0].user_id.name + ')')) return result @api.model def create(self, values): if values.get('is_posbox') and values.get( 'iface_customer_facing_display'): if values.get('customer_facing_display_html') and not values[ 'customer_facing_display_html'].strip(): values[ 'customer_facing_display_html'] = self._compute_default_customer_html( ) IrSequence = self.env['ir.sequence'].sudo() val = { 'name': _('POS Order %s') % values['name'], 'padding': 4, 'prefix': "%s/" % values['name'], 'code': "pos.order", 'company_id': values.get('company_id', False), } # force sequence_id field to new pos.order sequence values['sequence_id'] = IrSequence.create(val).id val.update(name=_('POS order line %s') % values['name'], code='pos.order.line') values['sequence_line_id'] = IrSequence.create(val).id pos_config = super(PosConfig, self).create(values) pos_config.sudo()._check_modules_to_install() pos_config.sudo()._check_groups_implied() # If you plan to add something after this, use a new environment. The one above is no longer valid after the modules install. return pos_config @api.multi def write(self, vals): if (self.is_posbox or vals.get('is_posbox')) and ( self.iface_customer_facing_display or vals.get('iface_customer_facing_display')): facing_display = (self.customer_facing_display_html or vals.get('customer_facing_display_html') or '').strip() if not facing_display: vals[ 'customer_facing_display_html'] = self._compute_default_customer_html( ) result = super(PosConfig, self).write(vals) self.sudo()._set_fiscal_position() self.sudo()._check_modules_to_install() self.sudo()._check_groups_implied() return result @api.multi def unlink(self): for pos_config in self.filtered( lambda pos_config: pos_config.sequence_id or pos_config. sequence_line_id): pos_config.sequence_id.unlink() pos_config.sequence_line_id.unlink() return super(PosConfig, self).unlink() def _set_fiscal_position(self): for config in self: if config.tax_regime and config.default_fiscal_position_id.id not in config.fiscal_position_ids.ids: config.fiscal_position_ids = [ (4, config.default_fiscal_position_id.id) ] elif not config.tax_regime_selection and not config.tax_regime and config.fiscal_position_ids.ids: config.fiscal_position_ids = [(5, 0, 0)] def _check_modules_to_install(self): module_installed = False for pos_config in self: for field_name in [ f for f in pos_config.fields_get_keys() if f.startswith('module_') ]: module_name = field_name.split('module_')[1] module_to_install = self.env['ir.module.module'].sudo().search( [('name', '=', module_name)]) if getattr(pos_config, field_name) and module_to_install.state not in ( 'installed', 'to install', 'to upgrade'): module_to_install.button_immediate_install() module_installed = True # just in case we want to do something if we install a module. (like a refresh ...) return module_installed def _check_groups_implied(self): for pos_config in self: for field_name in [ f for f in pos_config.fields_get_keys() if f.startswith('group_') ]: field = pos_config._fields[field_name] if field.type in ('boolean', 'selection') and hasattr( field, 'implied_group'): field_group_xmlids = getattr(field, 'group', 'base.group_user').split(',') field_groups = self.env['res.groups'].concat( *(self.env.ref(it) for it in field_group_xmlids)) field_groups.write({ 'implied_ids': [(4, self.env.ref(field.implied_group).id)] }) def execute(self): return { 'type': 'ir.actions.client', 'tag': 'reload', 'params': { 'wait': True } } # Methods to open the POS @api.multi def open_ui(self): """ open the pos interface """ self.ensure_one() return { 'type': 'ir.actions.act_url', 'url': '/pos/web/', 'target': 'self', } @api.multi def open_session_cb(self): """ new session button create one if none exist access cash control interface if enabled or start a session """ self.ensure_one() if not self.current_session_id: self.current_session_id = self.env['pos.session'].create({ 'user_id': self.env.uid, 'config_id': self.id }) if self.current_session_id.state == 'opened': return self.open_ui() return self._open_session(self.current_session_id.id) return self._open_session(self.current_session_id.id) @api.multi def open_existing_session_cb(self): """ close session button access session form to validate entries """ self.ensure_one() return self._open_session(self.current_session_id.id) def _open_session(self, session_id): return { 'name': _('Session'), 'view_type': 'form', 'view_mode': 'form,tree', 'res_model': 'pos.session', 'res_id': session_id, 'view_id': False, 'type': 'ir.actions.act_window', }
class LunchOrder(models.Model): """ A lunch order contains one or more lunch order line(s). It is associated to a user for a given date. When creating a lunch order, applicable lunch alerts are displayed. """ _name = 'lunch.order' _description = 'Lunch Order' _order = 'date desc' def _default_previous_order_ids(self): prev_order = self.env['lunch.order.line'].search( [('user_id', '=', self.env.uid), ('product_id.active', '!=', False)], limit=20, order='id desc') # If we return return prev_order.ids, we will have duplicates (identical orders). # Therefore, this following part removes duplicates based on product_id and note. return list({(order.product_id, order.note): order.id for order in prev_order}.values()) user_id = fields.Many2one('res.users', 'User', readonly=True, states={'new': [('readonly', False)]}, default=lambda self: self.env.uid) date = fields.Date('Date', required=True, readonly=True, states={'new': [('readonly', False)]}, default=fields.Date.context_today) order_line_ids = fields.One2many('lunch.order.line', 'order_id', 'Products', readonly=True, copy=True, states={ 'new': [('readonly', False)], False: [('readonly', False)] }) total = fields.Float(compute='_compute_total', string="Total", store=True) state = fields.Selection([('new', 'New'), ('confirmed', 'Received'), ('cancelled', 'Cancelled')], 'Status', readonly=True, index=True, copy=False, compute='_compute_order_state', store=True) alerts = fields.Text(compute='_compute_alerts_get', string="Alerts") company_id = fields.Many2one('res.company', related='user_id.company_id', store=True) currency_id = fields.Many2one('res.currency', related='company_id.currency_id', readonly=True, store=True) cash_move_balance = fields.Monetary(compute='_compute_cash_move_balance', multi='cash_move_balance') balance_visible = fields.Boolean(compute='_compute_cash_move_balance', multi='cash_move_balance') previous_order_ids = fields.Many2many('lunch.order.line', compute='_compute_previous_order') previous_order_widget = fields.Text(compute='_compute_previous_order') @api.one @api.depends('order_line_ids') def _compute_total(self): """ get and sum the order lines' price """ self.total = sum(orderline.price for orderline in self.order_line_ids) @api.multi def name_get(self): return [(order.id, '%s %s' % (_('Lunch Order'), '#%d' % order.id)) for order in self] @api.depends('state') def _compute_alerts_get(self): """ get the alerts to display on the order form """ alert_msg = [ alert.message for alert in self.env['lunch.alert'].search([]) if alert.display ] if self.state == 'new': self.alerts = alert_msg and '\n'.join(alert_msg) or False @api.multi @api.depends('user_id', 'state') def _compute_previous_order(self): self.ensure_one() self.previous_order_widget = json.dumps(False) prev_order = self.env['lunch.order.line'].search( [('user_id', '=', self.env.uid), ('product_id.active', '!=', False)], limit=20, order='date desc, id desc') # If we use prev_order.ids, we will have duplicates (identical orders). # Therefore, this following part removes duplicates based on product_id and note. self.previous_order_ids = list({(order.product_id, order.note): order.id for order in prev_order}.values()) if self.previous_order_ids: lunch_data = {} for line in self.previous_order_ids: lunch_data[line.id] = { 'line_id': line.id, 'product_id': line.product_id.id, 'product_name': line.product_id.name, 'supplier': line.supplier.name, 'note': line.note, 'price': line.price, 'date': line.date, 'currency_id': line.currency_id.id, } # sort the old lunch orders by (date, id) lunch_data = OrderedDict( sorted(lunch_data.items(), key=lambda t: (t[1]['date'], t[0]), reverse=True)) self.previous_order_widget = json.dumps(lunch_data) @api.one @api.depends('user_id') def _compute_cash_move_balance(self): domain = [('user_id', '=', self.user_id.id)] lunch_cash = self.env['lunch.cashmove'].read_group( domain, ['amount', 'user_id'], ['user_id']) if len(lunch_cash): self.cash_move_balance = lunch_cash[0]['amount'] self.balance_visible = (self.user_id == self.env.user) or self.user_has_groups( 'lunch.group_lunch_manager') @api.one @api.constrains('date') def _check_date(self): """ Prevents the user to create an order in the past """ date_order = datetime.datetime.strptime(self.date, '%Y-%m-%d') date_today = datetime.datetime.strptime( fields.Date.context_today(self), '%Y-%m-%d') if (date_order < date_today): raise ValidationError(_('The date of your order is in the past.')) @api.one @api.depends('order_line_ids.state') def _compute_order_state(self): """ Update the state of lunch.order based on its orderlines. Here is the logic: - if at least one order line is cancelled, the order is set as cancelled - if no line is cancelled but at least one line is not confirmed, the order is set as new - if all lines are confirmed, the order is set as confirmed """ if not self.order_line_ids: self.state = 'new' else: isConfirmed = True for orderline in self.order_line_ids: if orderline.state == 'cancelled': self.state = 'cancelled' return elif orderline.state == 'confirmed': continue else: isConfirmed = False if isConfirmed: self.state = 'confirmed' else: self.state = 'new' return
class HrPayslip(models.Model): _name = 'hr.payslip' _description = 'Pay Slip' struct_id = fields.Many2one( 'hr.payroll.structure', string='Structure', readonly=True, states={'draft': [('readonly', False)]}, help= 'Defines the rules that have to be applied to this payslip, accordingly ' 'to the contract chosen. If you let empty the field contract, this field isn\'t ' 'mandatory anymore and thus the rules applied will be all the rules set on the ' 'structure of all contracts of the employee valid for the chosen period' ) name = fields.Char(string='Payslip Name', readonly=True, states={'draft': [('readonly', False)]}) number = fields.Char(string='Reference', readonly=True, copy=False, states={'draft': [('readonly', False)]}) employee_id = fields.Many2one('hr.employee', string='Employee', required=True, readonly=True, states={'draft': [('readonly', False)]}) date_from = fields.Date(string='Date From', readonly=True, required=True, default=time.strftime('%Y-%m-01'), states={'draft': [('readonly', False)]}) date_to = fields.Date( string='Date To', readonly=True, required=True, default=str(datetime.now() + relativedelta.relativedelta(months=+1, day=1, days=-1)) [:10], states={'draft': [('readonly', False)]}) # this is chaos: 4 states are defined, 3 are used ('verify' isn't) and 5 exist ('confirm' seems to have existed) state = fields.Selection( [ ('draft', 'Draft'), ('verify', 'Waiting'), ('done', 'Done'), ('cancel', 'Rejected'), ], string='Status', index=True, readonly=True, copy=False, default='draft', help="""* When the payslip is created the status is \'Draft\' \n* If the payslip is under verification, the status is \'Waiting\'. \n* If the payslip is confirmed then status is set to \'Done\'. \n* When user cancel payslip the status is \'Rejected\'.""") line_ids = fields.One2many('hr.payslip.line', 'slip_id', string='Payslip Lines', readonly=True, states={'draft': [('readonly', False)]}) company_id = fields.Many2one( 'res.company', string='Company', readonly=True, copy=False, default=lambda self: self.env['res.company']._company_default_get(), states={'draft': [('readonly', False)]}) worked_days_line_ids = fields.One2many( 'hr.payslip.worked_days', 'payslip_id', string='Payslip Worked Days', copy=True, readonly=True, states={'draft': [('readonly', False)]}) input_line_ids = fields.One2many('hr.payslip.input', 'payslip_id', string='Payslip Inputs', readonly=True, states={'draft': [('readonly', False)]}) paid = fields.Boolean(string='Made Payment Order ? ', readonly=True, copy=False, states={'draft': [('readonly', False)]}) note = fields.Text(string='Internal Note', readonly=True, states={'draft': [('readonly', False)]}) contract_id = fields.Many2one('hr.contract', string='Contract', readonly=True, states={'draft': [('readonly', False)]}) details_by_salary_rule_category = fields.One2many( 'hr.payslip.line', compute='_compute_details_by_salary_rule_category', string='Details by Salary Rule Category') credit_note = fields.Boolean( string='Credit Note', readonly=True, states={'draft': [('readonly', False)]}, help="Indicates this payslip has a refund of another") payslip_run_id = fields.Many2one('hr.payslip.run', string='Payslip Batches', readonly=True, copy=False, states={'draft': [('readonly', False)]}) payslip_count = fields.Integer(compute='_compute_payslip_count', string="Payslip Computation Details") @api.multi def _compute_details_by_salary_rule_category(self): for payslip in self: payslip.details_by_salary_rule_category = payslip.mapped( 'line_ids').filtered(lambda line: line.category_id) @api.multi def _compute_payslip_count(self): for payslip in self: payslip.payslip_count = len(payslip.line_ids) @api.constrains('date_from', 'date_to') def _check_dates(self): if any( self.filtered( lambda payslip: payslip.date_from > payslip.date_to)): raise ValidationError( _("Payslip 'Date From' must be before 'Date To'.")) @api.multi def action_payslip_draft(self): return self.write({'state': 'draft'}) @api.multi def action_payslip_done(self): self.compute_sheet() return self.write({'state': 'done'}) @api.multi def action_payslip_cancel(self): if self.filtered(lambda slip: slip.state == 'done'): raise UserError(_("Cannot cancel a payslip that is done.")) return self.write({'state': 'cancel'}) @api.multi def refund_sheet(self): for payslip in self: copied_payslip = payslip.copy({ 'credit_note': True, 'name': _('Refund: ') + payslip.name }) copied_payslip.compute_sheet() copied_payslip.action_payslip_done() formview_ref = self.env.ref('hr_payroll.view_hr_payslip_form', False) treeview_ref = self.env.ref('hr_payroll.view_hr_payslip_tree', False) return { 'name': ("Refund Payslip"), 'view_mode': 'tree, form', 'view_id': False, 'view_type': 'form', 'res_model': 'hr.payslip', 'type': 'ir.actions.act_window', 'target': 'current', 'domain': "[('id', 'in', %s)]" % copied_payslip.ids, 'views': [(treeview_ref and treeview_ref.id or False, 'tree'), (formview_ref and formview_ref.id or False, 'form')], 'context': {} } @api.multi def check_done(self): return True @api.multi def unlink(self): if any( self.filtered(lambda payslip: payslip.state not in ('draft', 'cancel'))): raise UserError( _('You cannot delete a payslip which is not draft or cancelled!' )) return super(HrPayslip, self).unlink() # TODO move this function into hr_contract module, on hr.employee object @api.model def get_contract(self, employee, date_from, date_to): """ @param employee: recordset of employee @param date_from: date field @param date_to: date field @return: returns the ids of all the contracts for the given employee that need to be considered for the given dates """ # a contract is valid if it ends between the given dates clause_1 = [ '&', ('date_end', '<=', date_to), ('date_end', '>=', date_from) ] # OR if it starts between the given dates clause_2 = [ '&', ('date_start', '<=', date_to), ('date_start', '>=', date_from) ] # OR if it starts before the date_from and finish after the date_end (or never finish) clause_3 = [ '&', ('date_start', '<=', date_from), '|', ('date_end', '=', False), ('date_end', '>=', date_to) ] clause_final = [('employee_id', '=', employee.id), ('state', '=', 'open'), '|', '|' ] + clause_1 + clause_2 + clause_3 return self.env['hr.contract'].search(clause_final).ids @api.multi def compute_sheet(self): for payslip in self: number = payslip.number or self.env['ir.sequence'].next_by_code( 'salary.slip') # delete old payslip lines payslip.line_ids.unlink() # set the list of contract for which the rules have to be applied # if we don't give the contract, then the rules to apply should be for all current contracts of the employee contract_ids = payslip.contract_id.ids or \ self.get_contract(payslip.employee_id, payslip.date_from, payslip.date_to) lines = [ (0, 0, line) for line in self._get_payslip_lines(contract_ids, payslip.id) ] payslip.write({'line_ids': lines, 'number': number}) return True @api.model def get_worked_day_lines(self, contracts, date_from, date_to): """ @param contract: Browse record of contracts @return: returns a list of dict containing the input that should be applied for the given contract between date_from and date_to """ res = [] # fill only if the contract as a working schedule linked for contract in contracts.filtered( lambda contract: contract.resource_calendar_id): day_from = datetime.combine(fields.Date.from_string(date_from), datetime_time.min) day_to = datetime.combine(fields.Date.from_string(date_to), datetime_time.max) # compute leave days leaves = {} day_leave_intervals = contract.employee_id.iter_leaves( day_from, day_to, calendar=contract.resource_calendar_id) for day_intervals in day_leave_intervals: for interval in day_intervals: holiday = interval[2]['leaves'].holiday_id current_leave_struct = leaves.setdefault( holiday.holiday_status_id, { 'name': holiday.holiday_status_id.name, 'sequence': 5, 'code': holiday.holiday_status_id.name, 'number_of_days': 0.0, 'number_of_hours': 0.0, 'contract_id': contract.id, }) leave_time = (interval[1] - interval[0]).seconds / 3600 current_leave_struct['number_of_hours'] += leave_time work_hours = contract.employee_id.get_day_work_hours_count( interval[0].date(), calendar=contract.resource_calendar_id) if work_hours: current_leave_struct[ 'number_of_days'] += leave_time / work_hours # compute worked days work_data = contract.employee_id.get_work_days_data( day_from, day_to, calendar=contract.resource_calendar_id) attendances = { 'name': _("Normal Working Days paid at 100%"), 'sequence': 1, 'code': 'WORK100', 'number_of_days': work_data['days'], 'number_of_hours': work_data['hours'], 'contract_id': contract.id, } res.append(attendances) res.extend(leaves.values()) return res @api.model def get_inputs(self, contracts, date_from, date_to): res = [] structure_ids = contracts.get_all_structures() rule_ids = self.env['hr.payroll.structure'].browse( structure_ids).get_all_rules() sorted_rule_ids = [ id for id, sequence in sorted(rule_ids, key=lambda x: x[1]) ] inputs = self.env['hr.salary.rule'].browse(sorted_rule_ids).mapped( 'input_ids') for contract in contracts: for input in inputs: input_data = { 'name': input.name, 'code': input.code, 'contract_id': contract.id, } res += [input_data] return res @api.model def _get_payslip_lines(self, contract_ids, payslip_id): def _sum_salary_rule_category(localdict, category, amount): if category.parent_id: localdict = _sum_salary_rule_category(localdict, category.parent_id, amount) localdict['categories'].dict[ category.code] = category.code in localdict[ 'categories'].dict and localdict['categories'].dict[ category.code] + amount or amount return localdict class BrowsableObject(object): def __init__(self, employee_id, dict, env): self.employee_id = employee_id self.dict = dict self.env = env def __getattr__(self, attr): return attr in self.dict and self.dict.__getitem__(attr) or 0.0 class InputLine(BrowsableObject): """a class that will be used into the python code, mainly for usability purposes""" def sum(self, code, from_date, to_date=None): if to_date is None: to_date = fields.Date.today() self.env.cr.execute( """ SELECT sum(amount) as sum FROM hr_payslip as hp, hr_payslip_input as pi WHERE hp.employee_id = %s AND hp.state = 'done' AND hp.date_from >= %s AND hp.date_to <= %s AND hp.id = pi.payslip_id AND pi.code = %s""", (self.employee_id, from_date, to_date, code)) return self.env.cr.fetchone()[0] or 0.0 class WorkedDays(BrowsableObject): """a class that will be used into the python code, mainly for usability purposes""" def _sum(self, code, from_date, to_date=None): if to_date is None: to_date = fields.Date.today() self.env.cr.execute( """ SELECT sum(number_of_days) as number_of_days, sum(number_of_hours) as number_of_hours FROM hr_payslip as hp, hr_payslip_worked_days as pi WHERE hp.employee_id = %s AND hp.state = 'done' AND hp.date_from >= %s AND hp.date_to <= %s AND hp.id = pi.payslip_id AND pi.code = %s""", (self.employee_id, from_date, to_date, code)) return self.env.cr.fetchone() def sum(self, code, from_date, to_date=None): res = self._sum(code, from_date, to_date) return res and res[0] or 0.0 def sum_hours(self, code, from_date, to_date=None): res = self._sum(code, from_date, to_date) return res and res[1] or 0.0 class Payslips(BrowsableObject): """a class that will be used into the python code, mainly for usability purposes""" def sum(self, code, from_date, to_date=None): if to_date is None: to_date = fields.Date.today() self.env.cr.execute( """SELECT sum(case when hp.credit_note = False then (pl.total) else (-pl.total) end) FROM hr_payslip as hp, hr_payslip_line as pl WHERE hp.employee_id = %s AND hp.state = 'done' AND hp.date_from >= %s AND hp.date_to <= %s AND hp.id = pl.slip_id AND pl.code = %s""", (self.employee_id, from_date, to_date, code)) res = self.env.cr.fetchone() return res and res[0] or 0.0 #we keep a dict with the result because a value can be overwritten by another rule with the same code result_dict = {} rules_dict = {} worked_days_dict = {} inputs_dict = {} blacklist = [] payslip = self.env['hr.payslip'].browse(payslip_id) for worked_days_line in payslip.worked_days_line_ids: worked_days_dict[worked_days_line.code] = worked_days_line for input_line in payslip.input_line_ids: inputs_dict[input_line.code] = input_line categories = BrowsableObject(payslip.employee_id.id, {}, self.env) inputs = InputLine(payslip.employee_id.id, inputs_dict, self.env) worked_days = WorkedDays(payslip.employee_id.id, worked_days_dict, self.env) payslips = Payslips(payslip.employee_id.id, payslip, self.env) rules = BrowsableObject(payslip.employee_id.id, rules_dict, self.env) baselocaldict = { 'categories': categories, 'rules': rules, 'payslip': payslips, 'worked_days': worked_days, 'inputs': inputs } #get the ids of the structures on the contracts and their parent id as well contracts = self.env['hr.contract'].browse(contract_ids) if len(contracts) == 1 and payslip.struct_id: structure_ids = list( set(payslip.struct_id._get_parent_structure().ids)) else: structure_ids = contracts.get_all_structures() #get the rules of the structure and thier children rule_ids = self.env['hr.payroll.structure'].browse( structure_ids).get_all_rules() #run the rules by sequence sorted_rule_ids = [ id for id, sequence in sorted(rule_ids, key=lambda x: x[1]) ] sorted_rules = self.env['hr.salary.rule'].browse(sorted_rule_ids) for contract in contracts: employee = contract.employee_id localdict = dict(baselocaldict, employee=employee, contract=contract) for rule in sorted_rules: key = rule.code + '-' + str(contract.id) localdict['result'] = None localdict['result_qty'] = 1.0 localdict['result_rate'] = 100 #check if the rule can be applied if rule._satisfy_condition( localdict) and rule.id not in blacklist: #compute the amount of the rule amount, qty, rate = rule._compute_rule(localdict) #check if there is already a rule computed with that code previous_amount = rule.code in localdict and localdict[ rule.code] or 0.0 #set/overwrite the amount computed for this rule in the localdict tot_rule = amount * qty * rate / 100.0 localdict[rule.code] = tot_rule rules_dict[rule.code] = rule #sum the amount for its salary category localdict = _sum_salary_rule_category( localdict, rule.category_id, tot_rule - previous_amount) #create/overwrite the rule in the temporary results result_dict[key] = { 'salary_rule_id': rule.id, 'contract_id': contract.id, 'name': rule.name, 'code': rule.code, 'category_id': rule.category_id.id, 'sequence': rule.sequence, 'appears_on_payslip': rule.appears_on_payslip, 'condition_select': rule.condition_select, 'condition_python': rule.condition_python, 'condition_range': rule.condition_range, 'condition_range_min': rule.condition_range_min, 'condition_range_max': rule.condition_range_max, 'amount_select': rule.amount_select, 'amount_fix': rule.amount_fix, 'amount_python_compute': rule.amount_python_compute, 'amount_percentage': rule.amount_percentage, 'amount_percentage_base': rule.amount_percentage_base, 'register_id': rule.register_id.id, 'amount': amount, 'employee_id': contract.employee_id.id, 'quantity': qty, 'rate': rate, } else: #blacklist this rule and its children blacklist += [ id for id, seq in rule._recursive_search_of_rules() ] return list(result_dict.values()) # YTI TODO To rename. This method is not really an onchange, as it is not in any view # employee_id and contract_id could be browse records @api.multi def onchange_employee_id(self, date_from, date_to, employee_id=False, contract_id=False): #defaults res = { 'value': { 'line_ids': [], #delete old input lines 'input_line_ids': [( 2, x, ) for x in self.input_line_ids.ids], #delete old worked days lines 'worked_days_line_ids': [( 2, x, ) for x in self.worked_days_line_ids.ids], #'details_by_salary_head':[], TODO put me back 'name': '', 'contract_id': False, 'struct_id': False, } } if (not employee_id) or (not date_from) or (not date_to): return res ttyme = datetime.fromtimestamp( time.mktime(time.strptime(date_from, "%Y-%m-%d"))) employee = self.env['hr.employee'].browse(employee_id) locale = self.env.context.get('lang') or 'en_US' res['value'].update({ 'name': _('Salary Slip of %s for %s') % (employee.name, tools.ustr( babel.dates.format_date( date=ttyme, format='MMMM-y', locale=locale))), 'company_id': employee.company_id.id, }) if not self.env.context.get('contract'): #fill with the first contract of the employee contract_ids = self.get_contract(employee, date_from, date_to) else: if contract_id: #set the list of contract for which the input have to be filled contract_ids = [contract_id] else: #if we don't give the contract, then the input to fill should be for all current contracts of the employee contract_ids = self.get_contract(employee, date_from, date_to) if not contract_ids: return res contract = self.env['hr.contract'].browse(contract_ids[0]) res['value'].update({'contract_id': contract.id}) struct = contract.struct_id if not struct: return res res['value'].update({ 'struct_id': struct.id, }) #computation of the salary input contracts = self.env['hr.contract'].browse(contract_ids) worked_days_line_ids = self.get_worked_day_lines( contracts, date_from, date_to) input_line_ids = self.get_inputs(contracts, date_from, date_to) res['value'].update({ 'worked_days_line_ids': worked_days_line_ids, 'input_line_ids': input_line_ids, }) return res @api.onchange('employee_id', 'date_from', 'date_to') def onchange_employee(self): if (not self.employee_id) or (not self.date_from) or ( not self.date_to): return employee = self.employee_id date_from = self.date_from date_to = self.date_to contract_ids = [] ttyme = datetime.fromtimestamp( time.mktime(time.strptime(date_from, "%Y-%m-%d"))) locale = self.env.context.get('lang') or 'en_US' self.name = _('Salary Slip of %s for %s') % ( employee.name, tools.ustr( babel.dates.format_date( date=ttyme, format='MMMM-y', locale=locale))) self.company_id = employee.company_id if not self.env.context.get('contract') or not self.contract_id: contract_ids = self.get_contract(employee, date_from, date_to) if not contract_ids: return self.contract_id = self.env['hr.contract'].browse(contract_ids[0]) if not self.contract_id.struct_id: return self.struct_id = self.contract_id.struct_id #computation of the salary input contracts = self.env['hr.contract'].browse(contract_ids) worked_days_line_ids = self.get_worked_day_lines( contracts, date_from, date_to) worked_days_lines = self.worked_days_line_ids.browse([]) for r in worked_days_line_ids: worked_days_lines += worked_days_lines.new(r) self.worked_days_line_ids = worked_days_lines input_line_ids = self.get_inputs(contracts, date_from, date_to) input_lines = self.input_line_ids.browse([]) for r in input_line_ids: input_lines += input_lines.new(r) self.input_line_ids = input_lines return @api.onchange('contract_id') def onchange_contract(self): if not self.contract_id: self.struct_id = False self.with_context(contract=True).onchange_employee() return def get_salary_line_total(self, code): self.ensure_one() line = self.line_ids.filtered(lambda line: line.code == code) if line: return line[0].total else: return 0.0
class MailMail(models.Model): """Add the mass mailing campaign data to mail""" _inherit = ['mail.mail'] mailing_id = fields.Many2one('mail.mass_mailing', string='Mass Mailing') statistics_ids = fields.One2many('mail.mail.statistics', 'mail_mail_id', string='Statistics') @api.model def create(self, values): """ Override mail_mail creation to create an entry in mail.mail.statistics """ # TDE note: should be after 'all values computed', to have values (FIXME after merging other branch holding create refactoring) mail = super(MailMail, self).create(values) if values.get('statistics_ids'): mail_sudo = mail.sudo() mail_sudo.statistics_ids.write({'message_id': mail_sudo.message_id, 'state': 'outgoing'}) return mail def _get_tracking_url(self, partner=None): base_url = self.env['ir.config_parameter'].sudo().get_param('web.base.url') track_url = werkzeug.urls.url_join( base_url, 'mail/track/%(mail_id)s/blank.gif?%(params)s' % { 'mail_id': self.id, 'params': werkzeug.urls.url_encode({'db': self.env.cr.dbname}) } ) return '<img src="%s" alt=""/>' % track_url def _get_unsubscribe_url(self, email_to): base_url = self.env['ir.config_parameter'].sudo().get_param('web.base.url') url = werkzeug.urls.url_join( base_url, 'mail/mailing/%(mailing_id)s/unsubscribe?%(params)s' % { 'mailing_id': self.mailing_id.id, 'params': werkzeug.urls.url_encode({ 'db': self.env.cr.dbname, 'res_id': self.res_id, 'email': email_to, 'token': self.mailing_id._unsubscribe_token( self.res_id, email_to), }), } ) return url @api.multi def send_get_mail_body(self, partner=None): """ Override to add the tracking URL to the body and to add Statistic_id in shorted urls """ # TDE: temporary addition (mail was parameter) due to semi-new-API self.ensure_one() body = super(MailMail, self).send_get_mail_body(partner=partner) if self.mailing_id and body and self.statistics_ids: for match in re.findall(URL_REGEX, self.body_html): href = match[0] url = match[1] parsed = werkzeug.urls.url_parse(url, scheme='http') if parsed.scheme.startswith('http') and parsed.path.startswith('/r/'): new_href = href.replace(url, url + '/m/' + str(self.statistics_ids[0].id)) body = body.replace(href, new_href) # prepend <base> tag for images using absolute urls domain = self.env["ir.config_parameter"].sudo().get_param("web.base.url") base = "<base href='%s'>" % domain body = tools.append_content_to_html(base, body, plaintext=False, container_tag='div') # resolve relative image url to absolute for outlook.com def _sub_relative2absolute(match): return match.group(1) + werkzeug.urls.url_join(domain, match.group(2)) body = re.sub('(<img(?=\s)[^>]*\ssrc=")(/[^/][^"]+)', _sub_relative2absolute, body) body = re.sub(r'(<[^>]+\bstyle="[^"]+\burl\(\'?)(/[^/\'][^\'")]+)', _sub_relative2absolute, body) # generate tracking URL if self.statistics_ids: tracking_url = self._get_tracking_url(partner) if tracking_url: body = tools.append_content_to_html(body, tracking_url, plaintext=False, container_tag='div') return body @api.multi def send_get_email_dict(self, partner=None): # TDE: temporary addition (mail was parameter) due to semi-new-API res = super(MailMail, self).send_get_email_dict(partner) base_url = self.env['ir.config_parameter'].sudo().get_param('web.base.url') if self.mailing_id and res.get('body') and res.get('email_to'): emails = tools.email_split(res.get('email_to')[0]) email_to = emails and emails[0] or False unsubscribe_url = self._get_unsubscribe_url(email_to) link_to_replace = base_url + '/unsubscribe_from_list' if link_to_replace in res['body']: res['body'] = res['body'].replace(link_to_replace, unsubscribe_url if unsubscribe_url else '#') return res @api.multi def _postprocess_sent_message(self, mail_sent=True): for mail in self: if mail_sent is True and mail.statistics_ids: mail.statistics_ids.write({'sent': fields.Datetime.now(), 'exception': False}) elif mail_sent is False and mail.statistics_ids: mail.statistics_ids.write({'exception': fields.Datetime.now()}) return super(MailMail, self)._postprocess_sent_message(mail_sent=mail_sent)
class ImLivechatChannelRule(models.Model): """ Channel Rules Rules defining access to the channel (countries, and url matching). It also provide the 'auto pop' option to open automatically the conversation. """ _name = 'im_livechat.channel.rule' _description = 'Channel Rules' _order = 'sequence asc' regex_url = fields.Char( 'URL Regex', help= "Regular expression specifying the web pages this rule will be applied on." ) action = fields.Selection([('display_button', 'Display the button'), ('auto_popup', 'Auto popup'), ('hide_button', 'Hide the button')], string='Action', required=True, default='display_button', help="* 'Display the button' displays the chat button on the pages.\n"\ "* 'Auto popup' displays the button and automatically open the conversation pane.\n"\ "* 'Hide the button' hides the chat button on the pages.") auto_popup_timer = fields.Integer( 'Auto popup timer', default=0, help= "Delay (in seconds) to automatically open the conversation window. Note: the selected action must be 'Auto popup' otherwise this parameter will not be taken into account." ) channel_id = fields.Many2one('im_livechat.channel', 'Channel', help="The channel of the rule") country_ids = fields.Many2many( 'res.country', 'im_livechat_channel_country_rel', 'channel_id', 'country_id', 'Country', help= "The rule will only be applied for these countries. Example: if you select 'Belgium' and 'United States' and that you set the action to 'Hide Button', the chat button will be hidden on the specified URL from the visitors located in these 2 countries. This feature requires GeoIP installed on your server." ) sequence = fields.Integer( 'Matching order', default=10, help= "Given the order to find a matching rule. If 2 rules are matching for the given url/country, the one with the lowest sequence will be chosen." ) def match_rule(self, channel_id, url, country_id=False): """ determine if a rule of the given channel matches with the given url :param channel_id : the identifier of the channel_id :param url : the url to match with a rule :param country_id : the identifier of the country :returns the rule that matches the given condition. False otherwise. :rtype : im_livechat.channel.rule """ def _match(rules): for rule in rules: if re.search(rule.regex_url or '', url): return rule return False # first, search the country specific rules (the first match is returned) if country_id: # don't include the country in the research if geoIP is not installed domain = [('country_ids', 'in', [country_id]), ('channel_id', '=', channel_id)] rule = _match(self.search(domain)) if rule: return rule # second, fallback on the rules without country domain = [('country_ids', '=', False), ('channel_id', '=', channel_id)] return _match(self.search(domain))
class HrPayrollAdvice(models.Model): ''' Bank Advice ''' _name = 'hr.payroll.advice' def _get_default_date(self): return fields.Date.from_string(fields.Date.today()) name = fields.Char(readonly=True, required=True, states={'draft': [('readonly', False)]}) note = fields.Text( string='Description', default= 'Please make the payroll transfer from above account number to the below mentioned account numbers towards employee salaries:' ) date = fields.Date(readonly=True, required=True, states={'draft': [('readonly', False)]}, default=_get_default_date, help='Advice Date is used to search Payslips') state = fields.Selection([ ('draft', 'Draft'), ('confirm', 'Confirmed'), ('cancel', 'Cancelled'), ], string='Status', default='draft', index=True, readonly=True) number = fields.Char(string='Reference', readonly=True) line_ids = fields.One2many('hr.payroll.advice.line', 'advice_id', string='Employee Salary', states={'draft': [('readonly', False)]}, readonly=True, copy=True) chaque_nos = fields.Char(string='Cheque Numbers') neft = fields.Boolean( string='NEFT Transaction', help='Check this box if your company use online transfer for salary') company_id = fields.Many2one('res.company', string='Company', required=True, readonly=True, states={'draft': [('readonly', False)]}, default=lambda self: self.env.user.company_id) bank_id = fields.Many2one( 'res.bank', string='Bank', readonly=True, states={'draft': [('readonly', False)]}, help='Select the Bank from which the salary is going to be paid') batch_id = fields.Many2one('hr.payslip.run', string='Batch', readonly=True) @api.multi def compute_advice(self): """ Advice - Create Advice lines in Payment Advice and compute Advice lines. """ for advice in self: old_lines = self.env['hr.payroll.advice.line'].search([ ('advice_id', '=', advice.id) ]) if old_lines: old_lines.unlink() payslips = self.env['hr.payslip'].search([ ('date_from', '<=', advice.date), ('date_to', '>=', advice.date), ('state', '=', 'done') ]) for slip in payslips: if not slip.employee_id.bank_account_id and not slip.employee_id.bank_account_id.acc_number: raise UserError( _('Please define bank account for the %s employee') % (slip.employee_id.name, )) payslip_line = self.env['hr.payslip.line'].search( [('slip_id', '=', slip.id), ('code', '=', 'NET')], limit=1) if payslip_line: self.env['hr.payroll.advice.line'].create({ 'advice_id': advice.id, 'name': slip.employee_id.bank_account_id.acc_number, 'ifsc_code': slip.employee_id.bank_account_id.bank_bic or '', 'employee_id': slip.employee_id.id, 'bysal': payslip_line.total }) slip.advice_id = advice.id @api.multi def confirm_sheet(self): """ confirm Advice - confirmed Advice after computing Advice Lines.. """ for advice in self: if not advice.line_ids: raise UserError( _('You can not confirm Payment advice without advice lines.' )) date = fields.Date.from_string(fields.Date.today()) advice_year = date.strftime('%m') + '-' + date.strftime('%Y') number = self.env['ir.sequence'].next_by_code('payment.advice') advice.write({ 'number': 'PAY' + '/' + advice_year + '/' + number, 'state': 'confirm', }) @api.multi def set_to_draft(self): """Resets Advice as draft. """ self.write({'state': 'draft'}) @api.multi def cancel_sheet(self): """Marks Advice as cancelled. """ self.write({'state': 'cancel'}) @api.onchange('company_id') def _onchange_company_id(self): self.bank_id = self.company_id.partner_id.bank_ids and self.company_id.partner_id.bank_ids[ 0].bank_id.id or False
class ProductChangeQuantity(models.TransientModel): _name = "stock.change.product.qty" _description = "Change Product Quantity" # TDE FIXME: strange dfeault method, was present before migration ? to check product_id = fields.Many2one('product.product', 'Product', required=True) product_tmpl_id = fields.Many2one('product.template', 'Template', required=True) product_variant_count = fields.Integer( 'Variant Count', related='product_tmpl_id.product_variant_count') new_quantity = fields.Float( 'New Quantity on Hand', default=1, digits=dp.get_precision('Product Unit of Measure'), required=True, help= 'This quantity is expressed in the Default Unit of Measure of the product.' ) lot_id = fields.Many2one('stock.production.lot', 'Lot/Serial Number', domain="[('product_id','=',product_id)]") location_id = fields.Many2one('stock.location', 'Location', required=True, domain="[('usage', '=', 'internal')]") @api.model def default_get(self, fields): res = super(ProductChangeQuantity, self).default_get(fields) if not res.get('product_id') and self.env.context.get( 'active_id') and self.env.context.get( 'active_model' ) == 'product.template' and self.env.context.get('active_id'): res['product_id'] = self.env['product.product'].search( [('product_tmpl_id', '=', self.env.context['active_id'])], limit=1).id elif not res.get('product_id') and self.env.context.get( 'active_id') and self.env.context.get( 'active_model' ) == 'product.product' and self.env.context.get('active_id'): res['product_id'] = self.env['product.product'].browse( self.env.context['active_id']).id if 'location_id' in fields and not res.get('location_id'): company_user = self.env.user.company_id warehouse = self.env['stock.warehouse'].search( [('company_id', '=', company_user.id)], limit=1) if warehouse: res['location_id'] = warehouse.lot_stock_id.id return res @api.onchange('location_id', 'product_id') def onchange_location_id(self): # TDE FIXME: should'nt we use context / location ? if self.location_id and self.product_id: availability = self.product_id.with_context( compute_child=False)._product_available() self.new_quantity = availability[ self.product_id.id]['qty_available'] @api.onchange('product_id') def onchange_product_id(self): if self.product_id: self.product_tmpl_id = self.onchange_product_id_dict( self.product_id.id)['product_tmpl_id'] def _action_start_line(self): product = self.product_id.with_context(location=self.location_id.id, lot_id=self.lot_id.id) th_qty = product.qty_available res = { 'product_qty': self.new_quantity, 'location_id': self.location_id.id, 'product_id': self.product_id.id, 'product_uom_id': self.product_id.uom_id.id, 'theoretical_qty': th_qty, 'prod_lot_id': self.lot_id.id, } return res def onchange_product_id_dict(self, product_id): return { 'product_tmpl_id': self.env['product.product'].browse(product_id).product_tmpl_id.id, } @api.model def create(self, values): if values.get('product_id'): values.update(self.onchange_product_id_dict(values['product_id'])) return super(ProductChangeQuantity, self).create(values) @api.constrains('new_quantity') def check_new_quantity(self): if any(wizard.new_quantity < 0 for wizard in self): raise UserError(_('Quantity cannot be negative.')) def change_product_qty(self): """ Changes the Product Quantity by making a Physical Inventory. """ Inventory = self.env['stock.inventory'] for wizard in self: product = wizard.product_id.with_context( location=wizard.location_id.id, lot_id=wizard.lot_id.id) line_data = wizard._action_start_line() if wizard.product_id.id and wizard.lot_id.id: inventory_filter = 'none' elif wizard.product_id.id: inventory_filter = 'product' else: inventory_filter = 'none' inventory = Inventory.create({ 'name': _('INV: %s') % tools.ustr(wizard.product_id.display_name), 'filter': inventory_filter, 'product_id': wizard.product_id.id, 'location_id': wizard.location_id.id, 'lot_id': wizard.lot_id.id, 'line_ids': [(0, 0, line_data)], }) inventory.action_done() return {'type': 'ir.actions.act_window_close'}
class ProductPublicCategory(models.Model): _name = "product.public.category" _inherit = ["website.seo.metadata"] _description = "Website Product Category" _order = "sequence, name" def _default_website(self): default_website_id = self.env.ref('website.default_website') return [default_website_id.id] if default_website_id else None name = fields.Char(required=True, translate=True) parent_id = fields.Many2one('product.public.category', string='Parent Category', index=True) child_id = fields.One2many('product.public.category', 'parent_id', string='Children Categories') sequence = fields.Integer( help= "Gives the sequence order when displaying a list of product categories." ) # NOTE: there is no 'default image', because by default we don't show # thumbnails for categories. However if we have a thumbnail for at least one # category, then we display a default image on the other, so that the # buttons have consistent styling. # In this case, the default image is set by the js code. image = fields.Binary( attachment=True, help= "This field holds the image used as image for the category, limited to 1024x1024px." ) image_medium = fields.Binary( string='Medium-sized image', attachment=True, help="Medium-sized image of the category. It is automatically " "resized as a 128x128px image, with aspect ratio preserved. " "Use this field in form views or some kanban views.") image_small = fields.Binary( string='Small-sized image', attachment=True, help="Small-sized image of the category. It is automatically " "resized as a 64x64px image, with aspect ratio preserved. " "Use this field anywhere a small image is required.") website_ids = fields.Many2many('website', 'website_prod_public_categ_rel', 'website_id', 'category_id', default=_default_website, string='Websites', copy=False, help='List of websites in which ' 'category will published.') partner_tag_ids = fields.Many2many('res.partner.category', 'partner_public_categ_tags_rel', 'tag_id', 'category_id', string='Partner Tags', help='If logged in customers/partners ' 'have this tag then this product ' 'category will appear to them in ' 'E-commerce website.\n\n' 'If empty then it becomes general ' 'category which display to any ' 'customers/partners.') @api.model def create(self, vals): tools.image_resize_images(vals) res = super(ProductPublicCategory, self).create(vals) # @todo actpy: # Multi-Website: Check different test-cases for child & parent category if res.parent_id: res.parent_id.write({ 'website_ids': [(4, website_id.id) for website_id in res.website_ids] }) return res @api.multi def write(self, vals): tools.image_resize_images(vals) res = super(ProductPublicCategory, self).write(vals) # @todo actpy: # Multi-Website: Check different test-cases for child & parent category if self.parent_id and self.website_ids.ids: self.parent_id.write({ 'website_ids': [(4, website_id.id) for website_id in self.website_ids] }) if self.child_id: for child_id in self.child_id: for website_id in child_id.website_ids: if website_id not in self.website_ids: child_id.write({'website_ids': [(3, website_id.id)]}) return res @api.constrains('parent_id') def check_parent_id(self): if not self._check_recursion(): raise ValueError( _('Error ! You cannot create recursive categories.')) @api.multi def name_get(self): res = [] for category in self: names = [category.name] parent_category = category.parent_id while parent_category: names.append(parent_category.name) parent_category = parent_category.parent_id res.append((category.id, ' / '.join(reversed(names)))) return res
class Users(models.Model): """ User class. A res.users record models an OpenERP user and is different from an employee. res.users class now inherits from res.partner. The partner model is used to store the data related to the partner: lang, name, address, avatar, ... The user model is now dedicated to technical data. """ _name = "res.users" _description = 'Users' _inherits = {'res.partner': 'partner_id'} _order = 'name, login' __uid_cache = defaultdict(dict) # {dbname: {uid: password}} # User can write on a few of his own fields (but not his groups for example) SELF_WRITEABLE_FIELDS = ['signature', 'action_id', 'company_id', 'email', 'name', 'image', 'image_medium', 'image_small', 'lang', 'tz'] # User can read a few of his own fields SELF_READABLE_FIELDS = ['signature', 'company_id', 'login', 'email', 'name', 'image', 'image_medium', 'image_small', 'lang', 'tz', 'tz_offset', 'groups_id', 'partner_id', '__last_update', 'action_id'] def _default_groups(self): default_user = self.env.ref('base.default_user', raise_if_not_found=False) return (default_user or self.env['res.users']).sudo().groups_id def _companies_count(self): return self.env['res.company'].sudo().search_count([]) partner_id = fields.Many2one('res.partner', required=True, ondelete='restrict', auto_join=True, string='Related Partner', help='Partner-related data of the user') login = fields.Char(required=True, help="Used to log into the system") password = fields.Char(default='', invisible=True, copy=False, help="Keep empty if you don't want the user to be able to connect on the system.") new_password = fields.Char(string='Set Password', compute='_compute_password', inverse='_inverse_password', help="Specify a value only when creating a user or if you're "\ "changing the user's password, otherwise leave empty. After "\ "a change of password, the user has to login again.") signature = fields.Html() active = fields.Boolean(default=True) action_id = fields.Many2one('ir.actions.actions', string='Home Action', help="If specified, this action will be opened at log on for this user, in addition to the standard menu.") groups_id = fields.Many2many('res.groups', 'res_groups_users_rel', 'uid', 'gid', string='Groups', default=_default_groups) log_ids = fields.One2many('res.users.log', 'create_uid', string='User log entries') login_date = fields.Datetime(related='log_ids.create_date', string='Latest connection') share = fields.Boolean(compute='_compute_share', compute_sudo=True, string='Share User', store=True, help="External user with limited access, created only for the purpose of sharing data.") companies_count = fields.Integer(compute='_compute_companies_count', string="Number of Companies", default=_companies_count) tz_offset = fields.Char(compute='_compute_tz_offset', string='Timezone offset', invisible=True) @api.model def _get_company(self): return self.env.user.company_id # Special behavior for this field: res.company.search() will only return the companies # available to the current user (should be the user's companies?), when the user_preference # context is set. company_id = fields.Many2one('res.company', string='Company', required=True, default=_get_company, help='The company this user is currently working for.', context={'user_preference': True}) company_ids = fields.Many2many('res.company', 'res_company_users_rel', 'user_id', 'cid', string='Companies', default=_get_company) # overridden inherited fields to bypass access rights, in case you have # access to the user but not its corresponding partner name = fields.Char(related='partner_id.name', inherited=True) email = fields.Char(related='partner_id.email', inherited=True) _sql_constraints = [ ('login_key', 'UNIQUE (login)', 'You can not have two users with the same login !') ] def _compute_password(self): for user in self: user.password = '' def _inverse_password(self): for user in self: if not user.new_password: # Do not update the password if no value is provided, ignore silently. # For example web client submits False values for all empty fields. continue if user == self.env.user: # To change their own password, users must use the client-specific change password wizard, # so that the new password is immediately used for further RPC requests, otherwise the user # will face unexpected 'Access Denied' exceptions. raise UserError(_('Please use the change password wizard (in User Preferences or User menu) to change your own password.')) else: user.password = user.new_password @api.depends('groups_id') def _compute_share(self): for user in self: user.share = not user.has_group('base.group_user') @api.multi def _compute_companies_count(self): companies_count = self._companies_count() for user in self: user.companies_count = companies_count @api.depends('tz') def _compute_tz_offset(self): for user in self: user.tz_offset = datetime.datetime.now(pytz.timezone(user.tz or 'GMT')).strftime('%z') @api.onchange('login') def on_change_login(self): if self.login and tools.single_email_re.match(self.login): self.email = self.login @api.onchange('parent_id') def onchange_parent_id(self): return self.mapped('partner_id').onchange_parent_id() @api.multi @api.constrains('company_id', 'company_ids') def _check_company(self): if any(user.company_ids and user.company_id not in user.company_ids for user in self): raise ValidationError(_('The chosen company is not in the allowed companies for this user')) @api.multi @api.constrains('action_id') def _check_action_id(self): action_open_website = self.env.ref('base.action_open_website', raise_if_not_found=False) if action_open_website and any(user.action_id.id == action_open_website.id for user in self): raise ValidationError(_('The "App Switcher" action cannot be selected as home action.')) @api.multi def read(self, fields=None, load='_classic_read'): if fields and self == self.env.user: for key in fields: if not (key in self.SELF_READABLE_FIELDS or key.startswith('context_')): break else: # safe fields only, so we read as super-user to bypass access rights self = self.sudo() result = super(Users, self).read(fields=fields, load=load) canwrite = self.env['ir.model.access'].check('res.users', 'write', False) if not canwrite: for vals in result: if vals['id'] != self._uid: for key in USER_PRIVATE_FIELDS: if key in vals: vals[key] = '********' return result @api.model def read_group(self, domain, fields, groupby, offset=0, limit=None, orderby=False, lazy=True): groupby_fields = set([groupby] if isinstance(groupby, pycompat.string_types) else groupby) if groupby_fields.intersection(USER_PRIVATE_FIELDS): raise AccessError(_("Invalid 'group by' parameter")) return super(Users, self).read_group(domain, fields, groupby, offset=offset, limit=limit, orderby=orderby, lazy=lazy) @api.model def _search(self, args, offset=0, limit=None, order=None, count=False, access_rights_uid=None): if self._uid != SUPERUSER_ID and args: domain_fields = {term[0] for term in args if isinstance(term, (tuple, list))} if domain_fields.intersection(USER_PRIVATE_FIELDS): raise AccessError(_('Invalid search criterion')) return super(Users, self)._search(args, offset=offset, limit=limit, order=order, count=count, access_rights_uid=access_rights_uid) @api.model def create(self, vals): user = super(Users, self).create(vals) user.partner_id.active = user.active if user.partner_id.company_id: user.partner_id.write({'company_id': user.company_id.id}) return user @api.multi def write(self, values): if values.get('active') == False: for user in self: if user.id == SUPERUSER_ID: raise UserError(_("You cannot deactivate the admin user.")) elif user.id == self._uid: raise UserError(_("You cannot deactivate the user you're currently logged in as.")) if self == self.env.user: for key in list(values): if not (key in self.SELF_WRITEABLE_FIELDS or key.startswith('context_')): break else: if 'company_id' in values: if values['company_id'] not in self.env.user.company_ids.ids: del values['company_id'] # safe fields only, so we write as super-user to bypass access rights self = self.sudo() res = super(Users, self).write(values) if 'company_id' in values: for user in self: # if partner is global we keep it that way if user.partner_id.company_id and user.partner_id.company_id.id != values['company_id']: user.partner_id.write({'company_id': user.company_id.id}) # clear default ir values when company changes self.env['ir.default'].clear_caches() # clear caches linked to the users if 'groups_id' in values: self.env['ir.model.access'].call_cache_clearing_methods() self.env['ir.rule'].clear_caches() self.has_group.clear_cache(self) if any(key.startswith('context_') or key in ('lang', 'tz') for key in values): self.context_get.clear_cache(self) if any(key in values for key in ['active'] + USER_PRIVATE_FIELDS): db = self._cr.dbname for id in self.ids: self.__uid_cache[db].pop(id, None) if any(key in values for key in self._get_session_token_fields()): self._invalidate_session_cache() return res @api.multi def unlink(self): if SUPERUSER_ID in self.ids: raise UserError(_('You can not remove the admin user as it is used internally for resources created by actpy (updates, module installation, ...)')) db = self._cr.dbname for id in self.ids: self.__uid_cache[db].pop(id, None) self._invalidate_session_cache() return super(Users, self).unlink() @api.model def name_search(self, name='', args=None, operator='ilike', limit=100): if args is None: args = [] users = self.browse() if name and operator in ['=', 'ilike']: users = self.search([('login', '=', name)] + args, limit=limit) if not users: users = self.search([('name', operator, name)] + args, limit=limit) return users.name_get() @api.multi def copy(self, default=None): self.ensure_one() default = dict(default or {}) if ('name' not in default) and ('partner_id' not in default): default['name'] = _("%s (copy)") % self.name if 'login' not in default: default['login'] = _("%s (copy)") % self.login return super(Users, self).copy(default) @api.model @tools.ormcache('self._uid') def context_get(self): user = self.env.user result = {} for k in self._fields: if k.startswith('context_'): context_key = k[8:] elif k in ['lang', 'tz']: context_key = k else: context_key = False if context_key: res = getattr(user, k) or False if isinstance(res, models.BaseModel): res = res.id result[context_key] = res or False return result @api.model @api.returns('ir.actions.act_window', lambda record: record.id) def action_get(self): return self.sudo().env.ref('base.action_res_users_my') def check_super(self, passwd): return check_super(passwd) @api.model def check_credentials(self, password): """ Override this method to plug additional authentication methods""" user = self.sudo().search([('id', '=', self._uid), ('password', '=', password)]) if not user: raise AccessDenied() @api.model def _update_last_login(self): # only create new records to avoid any side-effect on concurrent transactions # extra records will be deleted by the periodical garbage collection self.env['res.users.log'].create({}) # populated by defaults @classmethod def _login(cls, db, login, password): if not password: return False user_id = False try: with cls.pool.cursor() as cr: self = api.Environment(cr, SUPERUSER_ID, {})[cls._name] user = self.search([('login', '=', login)]) if user: user_id = user.id user.sudo(user_id).check_credentials(password) user.sudo(user_id)._update_last_login() except AccessDenied: _logger.info("Login failed for db:%s login:%s", db, login) user_id = False return user_id @classmethod def authenticate(cls, db, login, password, user_agent_env): """Verifies and returns the user ID corresponding to the given ``login`` and ``password`` combination, or False if there was no matching user. :param str db: the database on which user is trying to authenticate :param str login: username :param str password: user password :param dict user_agent_env: environment dictionary describing any relevant environment attributes """ uid = cls._login(db, login, password) if uid == SUPERUSER_ID: # Successfully logged in as admin! # Attempt to guess the web base url... if user_agent_env and user_agent_env.get('base_location'): try: with cls.pool.cursor() as cr: base = user_agent_env['base_location'] ICP = api.Environment(cr, uid, {})['ir.config_parameter'] if not ICP.get_param('web.base.url.freeze'): ICP.set_param('web.base.url', base) except Exception: _logger.exception("Failed to update web.base.url configuration parameter") return uid @classmethod def check(cls, db, uid, passwd): """Verifies that the given (uid, password) is authorized for the database ``db`` and raise an exception if it is not.""" if not passwd: # empty passwords disallowed for obvious security reasons raise AccessDenied() db = cls.pool.db_name if cls.__uid_cache[db].get(uid) == passwd: return cr = cls.pool.cursor() try: self = api.Environment(cr, uid, {})[cls._name] self.check_credentials(passwd) cls.__uid_cache[db][uid] = passwd finally: cr.close() def _get_session_token_fields(self): return {'id', 'login', 'password', 'active'} @tools.ormcache('sid') def _compute_session_token(self, sid): """ Compute a session token given a session id and a user id """ # retrieve the fields used to generate the session token session_fields = ', '.join(sorted(self._get_session_token_fields())) self.env.cr.execute("""SELECT %s, (SELECT value FROM ir_config_parameter WHERE key='database.secret') FROM res_users WHERE id=%%s""" % (session_fields), (self.id,)) if self.env.cr.rowcount != 1: self._invalidate_session_cache() return False data_fields = self.env.cr.fetchone() # generate hmac key key = (u'%s' % (data_fields,)).encode('utf-8') # hmac the session id data = sid.encode('utf-8') h = hmac.new(key, data, sha256) # keep in the cache the token return h.hexdigest() @api.multi def _invalidate_session_cache(self): """ Clear the sessions cache """ self._compute_session_token.clear_cache(self) @api.model def change_password(self, old_passwd, new_passwd): """Change current user password. Old password must be provided explicitly to prevent hijacking an existing user session, or for cases where the cleartext password is not used to authenticate requests. :return: True :raise: actpy.exceptions.AccessDenied when old password is wrong :raise: actpy.exceptions.UserError when new password is not set or empty """ self.check(self._cr.dbname, self._uid, old_passwd) if new_passwd: # use self.env.user here, because it has uid=SUPERUSER_ID return self.env.user.write({'password': new_passwd}) raise UserError(_("Setting empty passwords is not allowed for security reasons!")) @api.multi def preference_save(self): return { 'type': 'ir.actions.client', 'tag': 'reload_context', } @api.multi def preference_change_password(self): return { 'type': 'ir.actions.client', 'tag': 'change_password', 'target': 'new', } @api.model def has_group(self, group_ext_id): # use singleton's id if called on a non-empty recordset, otherwise # context uid uid = self.id or self._uid return self.sudo(user=uid)._has_group(group_ext_id) @api.model @tools.ormcache('self._uid', 'group_ext_id') def _has_group(self, group_ext_id): """Checks whether user belongs to given group. :param str group_ext_id: external ID (XML ID) of the group. Must be provided in fully-qualified form (``module.ext_id``), as there is no implicit module to use.. :return: True if the current user is a member of the group with the given external ID (XML ID), else False. """ assert group_ext_id and '.' in group_ext_id, "External ID must be fully qualified" module, ext_id = group_ext_id.split('.') self._cr.execute("""SELECT 1 FROM res_groups_users_rel WHERE uid=%s AND gid IN (SELECT res_id FROM ir_model_data WHERE module=%s AND name=%s)""", (self._uid, module, ext_id)) return bool(self._cr.fetchone()) # for a few places explicitly clearing the has_group cache has_group.clear_cache = _has_group.clear_cache @api.multi def _is_public(self): self.ensure_one() return self.has_group('base.group_public') @api.multi def _is_system(self): self.ensure_one() return self.has_group('base.group_system') @api.multi def _is_admin(self): self.ensure_one() return self._is_superuser() or self.has_group('base.group_erp_manager') @api.multi def _is_superuser(self): self.ensure_one() return self.id == SUPERUSER_ID @api.model def get_company_currency_id(self): return self.env.user.company_id.currency_id.id
class PaymentAcquirer(models.Model): """ Acquirer Model. Each specific acquirer can extend the model by adding its own fields, using the acquirer_name as a prefix for the new fields. Using the required_if_provider='<name>' attribute on fields it is possible to have required fields that depend on a specific acquirer. Each acquirer has a link to an ir.ui.view record that is a template of a button used to display the payment form. See examples in ``payment_ogone`` and ``payment_paypal`` modules. Methods that should be added in an acquirer-specific implementation: - ``<name>_form_generate_values(self, reference, amount, currency, partner_id=False, partner_values=None, tx_custom_values=None)``: method that generates the values used to render the form button template. - ``<name>_get_form_action_url(self):``: method that returns the url of the button form. It is used for example in ecommerce application if you want to post some data to the acquirer. - ``<name>_compute_fees(self, amount, currency_id, country_id)``: computes the fees of the acquirer, using generic fields defined on the acquirer model (see fields definition). Each acquirer should also define controllers to handle communication between OpenERP and the acquirer. It generally consists in return urls given to the button form and that the acquirer uses to send the customer back after the transaction, with transaction details given as a POST request. """ _name = 'payment.acquirer' _description = 'Payment Acquirer' _order = 'website_published desc, sequence, name' name = fields.Char('Name', required=True, translate=True) description = fields.Html('Description') sequence = fields.Integer('Sequence', default=10, help="Determine the display order") provider = fields.Selection(selection=[('manual', 'Manual Configuration')], string='Provider', default='manual', required=True) company_id = fields.Many2one( 'res.company', 'Company', default=lambda self: self.env.user.company_id.id, required=True) view_template_id = fields.Many2one('ir.ui.view', 'Form Button Template', required=True) registration_view_template_id = fields.Many2one( 'ir.ui.view', 'S2S Form Template', domain=[('type', '=', 'qweb')], help="Template for method registration") environment = fields.Selection([('test', 'Test'), ('prod', 'Production')], string='Environment', default='test', oldname='env', required=True) website_published = fields.Boolean( 'Visible in Portal / Website', copy=False, help="Make this payment acquirer available (Customer invoices, etc.)") # Formerly associated to `authorize` option from auto_confirm capture_manually = fields.Boolean( string="Capture Amount Manually", help="Capture the amount from actpy, when the delivery is completed.") # Formerly associated to `generate_and_pay_invoice` option from auto_confirm journal_id = fields.Many2one( 'account.journal', 'Payment Journal', domain=[('type', 'in', ['bank', 'cash'])], default=lambda self: self.env['account.journal'].search( [('type', 'in', ['bank', 'cash'])], limit=1), help= """Payments will be registered into this journal. If you get paid straight on your bank account, select your bank account. If you get paid in batch for several transactions, create a specific payment journal for this payment acquirer to easily manage the bank reconciliation. You hold the amount in a temporary transfer account of your books (created automatically when you create the payment journal). Then when you get paid on your bank account by the payment acquirer, you reconcile the bank statement line with this temporary transfer account. Use reconciliation templates to do it in one-click.""") specific_countries = fields.Boolean( string="Specific Countries", help= "If you leave it empty, the payment acquirer will be available for all the countries." ) country_ids = fields.Many2many( 'res.country', 'payment_country_rel', 'payment_id', 'country_id', 'Countries', help= "This payment gateway is available for selected countries. If none is selected it is available for all countries." ) pre_msg = fields.Html( 'Help Message', translate=True, help='Message displayed to explain and help the payment process.') post_msg = fields.Html( 'Thanks Message', translate=True, help='Message displayed after having done the payment process.') pending_msg = fields.Html( 'Pending Message', translate=True, default=lambda s: _('<i>Pending,</i> Your online payment has been successfully processed. But your order is not validated yet.' ), help= 'Message displayed, if order is in pending state after having done the payment process.' ) done_msg = fields.Html( 'Done Message', translate=True, default=lambda s: _('<i>Done,</i> Your online payment has been successfully processed. Thank you for your order.' ), help= 'Message displayed, if order is done successfully after having done the payment process.' ) cancel_msg = fields.Html( 'Cancel Message', translate=True, default=lambda s: _('<i>Cancel,</i> Your payment has been cancelled.'), help='Message displayed, if order is cancel during the payment process.' ) error_msg = fields.Html( 'Error Message', translate=True, default=lambda s: _('<i>Error,</i> Please be aware that an error occurred during the transaction. The order has been confirmed but will not be paid. Do not hesitate to contact us if you have any questions on the status of your order.' ), help='Message displayed, if error is occur during the payment process.' ) save_token = fields.Selection( [('none', 'Never'), ('ask', 'Let the customer decide'), ('always', 'Always')], string='Save Cards', default='none', help= "This option allows customers to save their credit card as a payment token and to reuse it for a later purchase. " "If you manage subscriptions (recurring invoicing), you need it to automatically charge the customer when you " "issue an invoice.") token_implemented = fields.Boolean('Saving Card Data supported', compute='_compute_feature_support', search='_search_is_tokenized') authorize_implemented = fields.Boolean('Authorize Mechanism Supported', compute='_compute_feature_support') fees_implemented = fields.Boolean('Fees Computation Supported', compute='_compute_feature_support') fees_active = fields.Boolean('Add Extra Fees') fees_dom_fixed = fields.Float('Fixed domestic fees') fees_dom_var = fields.Float('Variable domestic fees (in percents)') fees_int_fixed = fields.Float('Fixed international fees') fees_int_var = fields.Float('Variable international fees (in percents)') # TDE FIXME: remove that brol module_id = fields.Many2one('ir.module.module', string='Corresponding Module') module_state = fields.Selection(selection=module.STATES, string='Installation State', related='module_id.state') image = fields.Binary( "Image", attachment=True, help= "This field holds the image used for this provider, limited to 1024x1024px" ) image_medium = fields.Binary( "Medium-sized image", attachment=True, help="Medium-sized image of this provider. It is automatically " "resized as a 128x128px image, with aspect ratio preserved. " "Use this field in form views or some kanban views.") image_small = fields.Binary( "Small-sized image", attachment=True, help="Small-sized image of this provider. It is automatically " "resized as a 64x64px image, with aspect ratio preserved. " "Use this field anywhere a small image is required.") payment_icon_ids = fields.Many2many('payment.icon', string='Supported Payment Icons') payment_flow = fields.Selection( selection=[('form', 'Redirection to the acquirer website'), ('s2s', 'Payment from Odoo')], default='form', required=True, string='Payment Flow', help= """Note: Subscriptions does not take this field in account, it uses server to server by default.""" ) def _search_is_tokenized(self, operator, value): tokenized = self._get_feature_support()['tokenize'] if (operator, value) in [('=', True), ('!=', False)]: return [('provider', 'in', tokenized)] return [('provider', 'not in', tokenized)] @api.multi def _compute_feature_support(self): feature_support = self._get_feature_support() for acquirer in self: acquirer.fees_implemented = acquirer.provider in feature_support[ 'fees'] acquirer.authorize_implemented = acquirer.provider in feature_support[ 'authorize'] acquirer.token_implemented = acquirer.provider in feature_support[ 'tokenize'] @api.multi def _check_required_if_provider(self): """ If the field has 'required_if_provider="<provider>"' attribute, then it required if record.provider is <provider>. """ for acquirer in self: if any( getattr(f, 'required_if_provider', None) == acquirer.provider and not acquirer[k] for k, f in self._fields.items()): return False return True _constraints = [ (_check_required_if_provider, 'Required fields not filled', []), ] def _get_feature_support(self): """Get advanced feature support by provider. Each provider should add its technical in the corresponding key for the following features: * fees: support payment fees computations * authorize: support authorizing payment (separates authorization and capture) * tokenize: support saving payment data in a payment.tokenize object """ return dict(authorize=[], tokenize=[], fees=[]) @api.model def create(self, vals): image_resize_images(vals) return super(PaymentAcquirer, self).create(vals) @api.multi def write(self, vals): image_resize_images(vals) return super(PaymentAcquirer, self).write(vals) @api.multi def toggle_website_published(self): self.write({'website_published': not self.website_published}) return True @api.multi def get_form_action_url(self): """ Returns the form action URL, for form-based acquirer implementations. """ if hasattr(self, '%s_get_form_action_url' % self.provider): return getattr(self, '%s_get_form_action_url' % self.provider)() return False def _get_available_payment_input(self, partner=None, company=None): """ Generic (model) method that fetches available payment mechanisms to use in all portal / eshop pages that want to use the payment form. It contains * form_acquirers: record set of acquirers based on a local form that sends customer to the acquirer website; * s2s_acquirers: reset set of acquirers that send customer data to acquirer without redirecting to any other website; * pms: record set of stored credit card data (aka payment.token) connected to a given partner to allow customers to reuse them """ if not company: company = self.env.user.company_id if not partner: partner = self.env.user.partner_id active_acquirers = self.sudo().search([ ('website_published', '=', True), ('company_id', '=', company.id) ]) form_acquirers = active_acquirers.filtered( lambda acq: acq.payment_flow == 'form' and acq.view_template_id) s2s_acquirers = active_acquirers.filtered( lambda acq: acq.payment_flow == 's2s' and acq. registration_view_template_id) return { 'form_acquirers': form_acquirers, 's2s_acquirers': s2s_acquirers, 'pms': self.env['payment.token'].search([('partner_id', '=', partner.id), ('acquirer_id', 'in', s2s_acquirers.ids)]), } @api.multi def render(self, reference, amount, currency_id, partner_id=False, values=None): """ Renders the form template of the given acquirer as a qWeb template. :param string reference: the transaction reference :param float amount: the amount the buyer has to pay :param currency_id: currency id :param dict partner_id: optional partner_id to fill values :param dict values: a dictionary of values for the transction that is given to the acquirer-specific method generating the form values All templates will receive: - acquirer: the payment.acquirer browse record - user: the current user browse record - currency_id: id of the transaction currency - amount: amount of the transaction - reference: reference of the transaction - partner_*: partner-related values - partner: optional partner browse record - 'feedback_url': feedback URL, controler that manage answer of the acquirer (without base url) -> FIXME - 'return_url': URL for coming back after payment validation (wihout base url) -> FIXME - 'cancel_url': URL if the client cancels the payment -> FIXME - 'error_url': URL if there is an issue with the payment -> FIXME - context: actpy context """ if values is None: values = {} # reference and amount values.setdefault('reference', reference) amount = float_round(amount, 2) values.setdefault('amount', amount) # currency id currency_id = values.setdefault('currency_id', currency_id) if currency_id: currency = self.env['res.currency'].browse(currency_id) else: currency = self.env.user.company_id.currency_id values['currency'] = currency # Fill partner_* using values['partner_id'] or partner_id argument partner_id = values.get('partner_id', partner_id) billing_partner_id = values.get('billing_partner_id', partner_id) if partner_id: partner = self.env['res.partner'].browse(partner_id) if partner_id != billing_partner_id: billing_partner = self.env['res.partner'].browse( billing_partner_id) else: billing_partner = partner values.update({ 'partner': partner, 'partner_id': partner_id, 'partner_name': partner.name, 'partner_lang': partner.lang, 'partner_email': partner.email, 'partner_zip': partner.zip, 'partner_city': partner.city, 'partner_address': _partner_format_address(partner.street, partner.street2), 'partner_country_id': partner.country_id.id, 'partner_country': partner.country_id, 'partner_phone': partner.phone, 'partner_state': partner.state_id, 'billing_partner': billing_partner, 'billing_partner_id': billing_partner_id, 'billing_partner_name': billing_partner.name, 'billing_partner_commercial_company_name': billing_partner.commercial_company_name, 'billing_partner_lang': billing_partner.lang, 'billing_partner_email': billing_partner.email, 'billing_partner_zip': billing_partner.zip, 'billing_partner_city': billing_partner.city, 'billing_partner_address': _partner_format_address(billing_partner.street, billing_partner.street2), 'billing_partner_country_id': billing_partner.country_id.id, 'billing_partner_country': billing_partner.country_id, 'billing_partner_phone': billing_partner.phone, 'billing_partner_state': billing_partner.state_id, }) if values.get('partner_name'): values.update({ 'partner_first_name': _partner_split_name(values.get('partner_name'))[0], 'partner_last_name': _partner_split_name(values.get('partner_name'))[1], }) if values.get('billing_partner_name'): values.update({ 'billing_partner_first_name': _partner_split_name(values.get('billing_partner_name'))[0], 'billing_partner_last_name': _partner_split_name(values.get('billing_partner_name'))[1], }) # Fix address, country fields if not values.get('partner_address'): values['address'] = _partner_format_address( values.get('partner_street', ''), values.get('partner_street2', '')) if not values.get('partner_country') and values.get( 'partner_country_id'): values['country'] = self.env['res.country'].browse( values.get('partner_country_id')) if not values.get('billing_partner_address'): values['billing_address'] = _partner_format_address( values.get('billing_partner_street', ''), values.get('billing_partner_street2', '')) if not values.get('billing_partner_country') and values.get( 'billing_partner_country_id'): values['billing_country'] = self.env['res.country'].browse( values.get('billing_partner_country_id')) # compute fees fees_method_name = '%s_compute_fees' % self.provider if hasattr(self, fees_method_name): fees = getattr(self, fees_method_name)(values['amount'], values['currency_id'], values.get('partner_country_id')) values['fees'] = float_round(fees, 2) # call <name>_form_generate_values to update the tx dict with acqurier specific values cust_method_name = '%s_form_generate_values' % (self.provider) if hasattr(self, cust_method_name): method = getattr(self, cust_method_name) values = method(values) values.update({ 'tx_url': self._context.get('tx_url', self.get_form_action_url()), 'submit_class': self._context.get('submit_class', 'btn btn-link'), 'submit_txt': self._context.get('submit_txt'), 'acquirer': self, 'user': self.env.user, 'context': self._context, 'type': values.get('type') or 'form', }) values.setdefault('return_url', False) return self.view_template_id.render(values, engine='ir.qweb') def get_s2s_form_xml_id(self): if self.registration_view_template_id: model_data = self.env['ir.model.data'].search([ ('model', '=', 'ir.ui.view'), ('res_id', '=', self.registration_view_template_id.id) ]) return ('%s.%s') % (model_data.module, model_data.name) return False @api.multi def s2s_process(self, data): cust_method_name = '%s_s2s_form_process' % (self.provider) if not self.s2s_validate(data): return False if hasattr(self, cust_method_name): # As this method may be called in JSON and overriden in various addons # let us raise interesting errors before having stranges crashes if not data.get('partner_id'): raise ValueError( _('Missing partner reference when trying to create a new payment token' )) method = getattr(self, cust_method_name) return method(data) return True @api.multi def s2s_validate(self, data): cust_method_name = '%s_s2s_form_validate' % (self.provider) if hasattr(self, cust_method_name): method = getattr(self, cust_method_name) return method(data) return True @api.multi def toggle_environment_value(self): prod = self.filtered(lambda acquirer: acquirer.environment == 'prod') prod.write({'environment': 'test'}) (self - prod).write({'environment': 'prod'}) @api.multi def button_immediate_install(self): # TDE FIXME: remove that brol if self.module_id and self.module_state != 'installed': self.module_id.button_immediate_install() return { 'type': 'ir.actions.client', 'tag': 'reload', }
class Groups(models.Model): _name = "res.groups" _description = "Access Groups" _rec_name = 'full_name' _order = 'name' name = fields.Char(required=True, translate=True) users = fields.Many2many('res.users', 'res_groups_users_rel', 'gid', 'uid') model_access = fields.One2many('ir.model.access', 'group_id', string='Access Controls', copy=True) rule_groups = fields.Many2many('ir.rule', 'rule_group_rel', 'group_id', 'rule_group_id', string='Rules', domain=[('global', '=', False)]) menu_access = fields.Many2many('ir.ui.menu', 'ir_ui_menu_group_rel', 'gid', 'menu_id', string='Access Menu') view_access = fields.Many2many('ir.ui.view', 'ir_ui_view_group_rel', 'group_id', 'view_id', string='Views') comment = fields.Text(translate=True) category_id = fields.Many2one('ir.module.category', string='Application', index=True) color = fields.Integer(string='Color Index') full_name = fields.Char(compute='_compute_full_name', string='Group Name', search='_search_full_name') share = fields.Boolean(string='Share Group', help="Group created to set access rights for sharing data with some users.") is_portal = fields.Boolean('Portal', help="If checked, this group is usable as a portal.") _sql_constraints = [ ('name_uniq', 'unique (category_id, name)', 'The name of the group must be unique within an application!') ] @api.depends('category_id.name', 'name') def _compute_full_name(self): # Important: value must be stored in environment of group, not group1! for group, group1 in pycompat.izip(self, self.sudo()): if group1.category_id: group.full_name = '%s / %s' % (group1.category_id.name, group1.name) else: group.full_name = group1.name def _search_full_name(self, operator, operand): lst = True if isinstance(operand, bool): domains = [[('name', operator, operand)], [('category_id.name', operator, operand)]] if operator in expression.NEGATIVE_TERM_OPERATORS == (not operand): return expression.AND(domains) else: return expression.OR(domains) if isinstance(operand, pycompat.string_types): lst = False operand = [operand] where = [] for group in operand: values = [v for v in group.split('/') if v] group_name = values.pop().strip() category_name = values and '/'.join(values).strip() or group_name group_domain = [('name', operator, lst and [group_name] or group_name)] category_domain = [('category_id.name', operator, lst and [category_name] or category_name)] if operator in expression.NEGATIVE_TERM_OPERATORS and not values: category_domain = expression.OR([category_domain, [('category_id', '=', False)]]) if (operator in expression.NEGATIVE_TERM_OPERATORS) == (not values): sub_where = expression.AND([group_domain, category_domain]) else: sub_where = expression.OR([group_domain, category_domain]) if operator in expression.NEGATIVE_TERM_OPERATORS: where = expression.AND([where, sub_where]) else: where = expression.OR([where, sub_where]) return where @api.model def search(self, args, offset=0, limit=None, order=None, count=False): # add explicit ordering if search is sorted on full_name if order and order.startswith('full_name'): groups = super(Groups, self).search(args) groups = groups.sorted('full_name', reverse=order.endswith('DESC')) groups = groups[offset:offset+limit] if limit else groups[offset:] return len(groups) if count else groups.ids return super(Groups, self).search(args, offset=offset, limit=limit, order=order, count=count) @api.multi def copy(self, default=None): self.ensure_one() default = dict(default or {}, name=_('%s (copy)') % self.name) return super(Groups, self).copy(default) @api.multi def write(self, vals): if 'name' in vals: if vals['name'].startswith('-'): raise UserError(_('The name of the group can not start with "-"')) # invalidate caches before updating groups, since the recomputation of # field 'share' depends on method has_group() self.env['ir.model.access'].call_cache_clearing_methods() self.env['res.users'].has_group.clear_cache(self.env['res.users']) return super(Groups, self).write(vals)
class PaymentToken(models.Model): _name = 'payment.token' _order = 'partner_id, id desc' name = fields.Char('Name', help='Name of the payment token') short_name = fields.Char('Short name', compute='_compute_short_name') partner_id = fields.Many2one('res.partner', 'Partner', required=True) acquirer_id = fields.Many2one('payment.acquirer', 'Acquirer Account', required=True) acquirer_ref = fields.Char('Acquirer Ref.', required=True) active = fields.Boolean('Active', default=True) payment_ids = fields.One2many('payment.transaction', 'payment_token_id', 'Payment Transactions') verified = fields.Boolean(string='Verified', default=False) @api.model def create(self, values): # call custom create method if defined (i.e. ogone_create for ogone) if values.get('acquirer_id'): acquirer = self.env['payment.acquirer'].browse( values['acquirer_id']) # custom create custom_method_name = '%s_create' % acquirer.provider if hasattr(self, custom_method_name): values.update(getattr(self, custom_method_name)(values)) # remove all non-model fields used by (provider)_create method to avoid warning fields_wl = set(self._fields) & set(values) values = {field: values[field] for field in fields_wl} return super(PaymentToken, self).create(values) """ @TBE: stolen shamelessly from there https://www.paypal.com/us/selfhelp/article/why-is-there-a-$1.95-charge-on-my-card-statement-faq554 Most of them are ~1.50€s TODO: See this with @AL & @DBO """ VALIDATION_AMOUNTS = { 'CAD': 2.45, 'EUR': 1.50, 'GBP': 1.00, 'JPY': 200, 'AUD': 2.00, 'NZD': 3.00, 'CHF': 3.00, 'HKD': 15.00, 'SEK': 15.00, 'DKK': 12.50, 'PLN': 6.50, 'NOK': 15.00, 'HUF': 400.00, 'CZK': 50.00, 'BRL': 4.00, 'MYR': 10.00, 'MXN': 20.00, 'ILS': 8.00, 'PHP': 100.00, 'TWD': 70.00, 'THB': 70.00 } @api.model def validate(self, **kwargs): """ This method allow to verify if this payment method is valid or not. It does this by withdrawing a certain amount and then refund it right after. """ currency = self.partner_id.currency_id if self.VALIDATION_AMOUNTS.get(currency.name): amount = self.VALIDATION_AMOUNTS.get(currency.name) else: # If we don't find the user's currency, then we set the currency to EUR and the amount to 1€50. currency = self.env['res.currency'].search([('name', '=', 'EUR')]) amount = 1.5 if len(currency) != 1: _logger.error( "Error 'EUR' currency not found for payment method validation!" ) return False reference = "VALIDATION-%s-%s" % ( self.id, datetime.datetime.now().strftime('%y%m%d_%H%M%S')) tx = self.env['payment.transaction'].sudo().create({ 'amount': amount, 'acquirer_id': self.acquirer_id.id, 'type': 'validation', 'currency_id': currency.id, 'reference': reference, 'payment_token_id': self.id, 'partner_id': self.partner_id.id, 'partner_country_id': self.partner_id.country_id.id, }) kwargs.update({'3d_secure': True}) tx.s2s_do_transaction(**kwargs) # if 3D secure is called, then we do not refund right now if not tx.html_3ds: tx.s2s_do_refund() return tx @api.multi @api.depends('name') def _compute_short_name(self): for token in self: token.short_name = token.name.replace('XXXXXXXXXXXX', '***') @api.multi def get_linked_records(self): """ This method returns a dict containing all the records linked to the payment.token (e.g Subscriptions), the key is the id of the payment.token and the value is an array that must follow the scheme below. { token_id: [ 'description': The model description (e.g 'Sale Subscription'), 'id': The id of the record, 'name': The name of the record, 'url': The url to access to this record. ] } """ return {r.id: [] for r in self}
class ResCompany(models.Model): _inherit = 'res.company' # To do in master : refactor to set sequences more generic l10n_fr_secure_sequence_id = fields.Many2one( 'ir.sequence', 'Sequence to use to ensure the securisation of data', readonly=True) @api.model def create(self, vals): company = super(ResCompany, self).create(vals) #when creating a new french company, create the securisation sequence as well if company._is_accounting_unalterable(): sequence_fields = ['l10n_fr_secure_sequence_id'] company._create_secure_sequence(sequence_fields) return company @api.multi def write(self, vals): res = super(ResCompany, self).write(vals) #if country changed to fr, create the securisation sequence for company in self: if company._is_accounting_unalterable(): sequence_fields = ['l10n_fr_secure_sequence_id'] company._create_secure_sequence(sequence_fields) return res def _create_secure_sequence(self, sequence_fields): """This function creates a no_gap sequence on each companies in self that will ensure a unique number is given to all posted account.move in such a way that we can always find the previous move of a journal entry. """ for company in self: vals_write = {} for seq_field in sequence_fields: if not company[seq_field]: vals = { 'name': 'French Securisation of ' + seq_field + ' - ' + company.name, 'code': 'FRSECUR', 'implementation': 'no_gap', 'prefix': '', 'suffix': '', 'padding': 0, 'company_id': company.id } seq = self.env['ir.sequence'].create(vals) vals_write[seq_field] = seq.id if vals_write: company.write(vals_write) def _is_vat_french(self): return self.vat and self.vat.startswith('FR') and len(self.vat) == 13 def _is_accounting_unalterable(self): if not self.vat and not self.country_id: return False return self.country_id and self.country_id.code in UNALTERABLE_COUNTRIES or self._is_vat_french( )