class MembershipLine(models.Model): _name = 'membership.membership_line' _rec_name = 'partner' _order = 'id desc' _description = 'Membership Line' partner = fields.Many2one('res.partner', string='Partner', ondelete='cascade', index=True) membership_id = fields.Many2one('product.product', string="Membership", required=True) date_from = fields.Date(string='From', readonly=True) date_to = fields.Date(string='To', readonly=True) date_cancel = fields.Date(string='Cancel date') date = fields.Date(string='Join Date', help="Date on which member has joined the membership") member_price = fields.Float(string='Membership Fee', digits='Product Price', required=True, help='Amount for the membership') account_invoice_line = fields.Many2one('account.move.line', string='Account Invoice line', readonly=True, ondelete='cascade') account_invoice_id = fields.Many2one( 'account.move', related='account_invoice_line.move_id', string='Invoice', readonly=True) company_id = fields.Many2one( 'res.company', related='account_invoice_line.move_id.company_id', string="Company", readonly=True, store=True) state = fields.Selection( STATE, compute='_compute_state', string='Membership Status', store=True, help="It indicates the membership status.\n" "-Non Member: A member who has not applied for any membership.\n" "-Cancelled Member: A member who has cancelled his membership.\n" "-Old Member: A member whose membership date has expired.\n" "-Waiting Member: A member who has applied for the membership and whose invoice is going to be created.\n" "-Invoiced Member: A member whose invoice has been created.\n" "-Paid Member: A member who has paid the membership amount.") @api.depends('account_invoice_id.state', 'account_invoice_id.amount_residual', 'account_invoice_id.invoice_payment_state') def _compute_state(self): """Compute the state lines """ if not self: return self._cr.execute( ''' SELECT reversed_entry_id, COUNT(id) FROM account_move WHERE reversed_entry_id IN %s GROUP BY reversed_entry_id ''', [tuple(self.mapped('account_invoice_id.id'))]) reverse_map = dict(self._cr.fetchall()) for line in self: move_state = line.account_invoice_id.state payment_state = line.account_invoice_id.invoice_payment_state line.state = 'none' if move_state == 'draft': line.state = 'waiting' elif move_state == 'posted': if payment_state == 'paid': if reverse_map.get(line.account_invoice_id.id): line.state = 'canceled' else: line.state = 'paid' elif payment_state == 'in_payment': line.state = 'paid' elif payment_state == 'not_paid': line.state = 'invoiced' elif move_state == 'cancel': line.state = 'canceled'
class TrialBalanceReportAccount(models.TransientModel): _name = 'report_trial_balance_account' _inherit = 'account_financial_report_abstract' _order = 'sequence, code ASC, name' report_id = fields.Many2one( comodel_name='report_trial_balance', ondelete='cascade', index=True ) hide_line = fields.Boolean(compute='_compute_hide_line') # Data fields, used to keep link with real object. # Sequence is a Char later built with 'parent_path' for groups # and parent_path + account code for accounts sequence = fields.Char(index=True, default='1') level = fields.Integer(index=True, default=1) # Data fields, used to keep link with real object account_id = fields.Many2one( 'account.account', index=True ) account_group_id = fields.Many2one( 'account.group', index=True ) parent_id = fields.Many2one( 'account.group', index=True ) child_account_ids = fields.Char( string="Child accounts") compute_account_ids = fields.Many2many( 'account.account', string="Compute accounts", store=True) # Data fields, used for report display code = fields.Char() name = fields.Char() currency_id = fields.Many2one('res.currency') initial_balance = fields.Float(digits=(16, 2)) initial_balance_foreign_currency = fields.Float(digits=(16, 2)) debit = fields.Float(digits=(16, 2)) credit = fields.Float(digits=(16, 2)) period_balance = fields.Float(digits=(16, 2)) final_balance = fields.Float(digits=(16, 2)) final_balance_foreign_currency = fields.Float(digits=(16, 2)) # Data fields, used to browse report data partner_ids = fields.One2many( comodel_name='report_trial_balance_partner', inverse_name='report_account_id' ) @api.depends( 'currency_id', 'report_id', 'report_id.hide_account_at_0', 'report_id.limit_hierarchy_level', 'report_id.show_hierarchy_level', 'initial_balance', 'final_balance', 'debit', 'credit', ) def _compute_hide_line(self): for rec in self: report = rec.report_id r = (rec.currency_id or report.company_id.currency_id).rounding if report.hide_account_at_0 and ( float_is_zero(rec.initial_balance, precision_rounding=r) and float_is_zero(rec.final_balance, precision_rounding=r) and float_is_zero(rec.debit, precision_rounding=r) and float_is_zero(rec.credit, precision_rounding=r)): rec.hide_line = True elif report.limit_hierarchy_level and report.show_hierarchy_level: if report.hide_parent_hierarchy_level: distinct_level = rec.level != report.show_hierarchy_level if rec.account_group_id and distinct_level: rec.hide_line = True elif rec.level and distinct_level: rec.hide_line = True elif not report.hide_parent_hierarchy_level and \ rec.level > report.show_hierarchy_level: rec.hide_line = True
class PosPayment(models.Model): """ Used to register payments made in a pos.order. See `payment_ids` field of pos.order model. The main characteristics of pos.payment can be read from `payment_method_id`. """ _name = "pos.payment" _description = "Point of Sale Payments" _order = "id desc" name = fields.Char(string='Label', readonly=True) pos_order_id = fields.Many2one('pos.order', string='Order', required=True) amount = fields.Monetary(string='Amount', required=True, currency_field='currency_id', readonly=True, help="Total amount of the payment.") payment_method_id = fields.Many2one('pos.payment.method', string='Payment Method', required=True) payment_date = fields.Datetime(string='Date', required=True, readonly=True, default=lambda self: fields.Datetime.now()) currency_id = fields.Many2one('res.currency', string='Currency', related='pos_order_id.currency_id') currency_rate = fields.Float( string='Conversion Rate', related='pos_order_id.currency_rate', help='Conversion rate from company currency to order currency.') partner_id = fields.Many2one('res.partner', string='Customer', related='pos_order_id.partner_id') session_id = fields.Many2one('pos.session', string='Session', related='pos_order_id.session_id', store=True) company_id = fields.Many2one('res.company', string='Company', related='pos_order_id.company_id') card_type = fields.Char('Type of card used') transaction_id = fields.Char('Payment Transaction ID') @api.model def name_get(self): res = [] for payment in self: if payment.name: res.append((payment.id, _('%s %s') % (payment.name, formatLang(self.env, payment.amount, currency_obj=payment.currency_id)))) else: res.append((payment.id, formatLang(self.env, payment.amount, currency_obj=payment.currency_id))) return res
class AssetAssetReport(models.Model): _name = "asset.asset.report" _description = "Assets Analysis" _auto = False name = fields.Char(string='Year', required=False, readonly=True) date = fields.Date(readonly=True) depreciation_date = fields.Date(string='Depreciation Date', readonly=True) asset_id = fields.Many2one('account.asset.asset', string='Asset', readonly=True) asset_category_id = fields.Many2one('account.asset.category', string='Asset category', readonly=True) partner_id = fields.Many2one('res.partner', string='Partner', readonly=True) state = fields.Selection([('draft', 'Draft'), ('open', 'Running'), ('close', 'Close')], string='Status', readonly=True) depreciation_value = fields.Float(string='Amount of Depreciation Lines', readonly=True) installment_value = fields.Float(string='Amount of Installment Lines', readonly=True) move_check = fields.Boolean(string='Posted', readonly=True) installment_nbr = fields.Integer(string='Installment Count', readonly=True) depreciation_nbr = fields.Integer(string='Depreciation Count', readonly=True) gross_value = fields.Float(string='Gross Amount', readonly=True) posted_value = fields.Float(string='Posted Amount', readonly=True) unposted_value = fields.Float(string='Unposted Amount', readonly=True) company_id = fields.Many2one('res.company', string='Company', readonly=True) def init(self): tools.drop_view_if_exists(self._cr, 'asset_asset_report') self._cr.execute(""" create or replace view asset_asset_report as ( select min(dl.id) as id, dl.name as name, dl.depreciation_date as depreciation_date, a.date as date, (CASE WHEN dlmin.id = min(dl.id) THEN a.value ELSE 0 END) as gross_value, dl.amount as depreciation_value, dl.amount as installment_value, (CASE WHEN dl.move_check THEN dl.amount ELSE 0 END) as posted_value, (CASE WHEN NOT dl.move_check THEN dl.amount ELSE 0 END) as unposted_value, dl.asset_id as asset_id, dl.move_check as move_check, a.category_id as asset_category_id, a.partner_id as partner_id, a.state as state, count(dl.*) as installment_nbr, count(dl.*) as depreciation_nbr, a.company_id as company_id from account_asset_depreciation_line dl left join account_asset_asset a on (dl.asset_id=a.id) left join (select min(d.id) as id,ac.id as ac_id from account_asset_depreciation_line as d inner join account_asset_asset as ac ON (ac.id=d.asset_id) group by ac_id) as dlmin on dlmin.ac_id=a.id where a.active is true group by dl.amount,dl.asset_id,dl.depreciation_date,dl.name, a.date, dl.move_check, a.state, a.category_id, a.partner_id, a.company_id, a.value, a.id, a.salvage_value, dlmin.id )""")
class LeaveReport(models.Model): _name = "hr.leave.report" _description = 'Time Off Summary / Report' _auto = False _order = "date_from DESC, employee_id" employee_id = fields.Many2one('hr.employee', string="Employee", readonly=True) name = fields.Char('Description', readonly=True) number_of_days = fields.Float('Number of Days', readonly=True) leave_type = fields.Selection([ ('allocation', 'Allocation Request'), ('request', 'Time Off Request') ], string='Request Type', readonly=True) department_id = fields.Many2one('hr.department', string='Department', readonly=True) category_id = fields.Many2one('hr.employee.category', string='Employee Tag', readonly=True) holiday_status_id = fields.Many2one("hr.leave.type", string="Leave Type", readonly=True) state = fields.Selection([ ('draft', 'To Submit'), ('cancel', 'Cancelled'), ('confirm', 'To Approve'), ('refuse', 'Refused'), ('validate1', 'Second Approval'), ('validate', 'Approved') ], string='Status', readonly=True) holiday_type = fields.Selection([ ('employee', 'By Employee'), ('category', 'By Employee Tag') ], string='Allocation Mode', readonly=True) date_from = fields.Datetime('Start Date', readonly=True) date_to = fields.Datetime('End Date', readonly=True) payslip_status = fields.Boolean('Reported in last payslips', readonly=True) def init(self): tools.drop_view_if_exists(self._cr, 'hr_leave_report') self._cr.execute(""" CREATE or REPLACE view hr_leave_report as ( SELECT row_number() over(ORDER BY leaves.employee_id) as id, leaves.employee_id as employee_id, leaves.name as name, leaves.number_of_days as number_of_days, leaves.leave_type as leave_type, leaves.category_id as category_id, leaves.department_id as department_id, leaves.holiday_status_id as holiday_status_id, leaves.state as state, leaves.holiday_type as holiday_type, leaves.date_from as date_from, leaves.date_to as date_to, leaves.payslip_status as payslip_status from (select allocation.employee_id as employee_id, allocation.name as name, allocation.number_of_days as number_of_days, allocation.category_id as category_id, allocation.department_id as department_id, allocation.holiday_status_id as holiday_status_id, allocation.state as state, allocation.holiday_type, null as date_from, null as date_to, FALSE as payslip_status, 'allocation' as leave_type from hr_leave_allocation as allocation union all select request.employee_id as employee_id, request.name as name, (request.number_of_days * -1) as number_of_days, request.category_id as category_id, request.department_id as department_id, request.holiday_status_id as holiday_status_id, request.state as state, request.holiday_type, request.date_from as date_from, request.date_to as date_to, request.payslip_status as payslip_status, 'request' as leave_type from hr_leave as request) leaves ); """) def _read_from_database(self, field_names, inherited_field_names=[]): if 'name' in field_names and 'employee_id' not in field_names: field_names.append('employee_id') super(LeaveReport, self)._read_from_database(field_names, inherited_field_names) if 'name' in field_names: if self.user_has_groups('hr_holidays.group_hr_holidays_user'): return current_employee = self.env['hr.employee'].sudo().search([('user_id', '=', self.env.uid)], limit=1) for record in self: emp_id = record._cache.get('employee_id', [False])[0] if emp_id != current_employee.id: try: record._cache['name'] record._cache['name'] = '*****' except Exception: # skip SpecialValue (e.g. for missing record or access right) pass @api.model def action_time_off_analysis(self): domain = [('holiday_type', '=', 'employee')] if self.env.context.get('active_ids'): domain = expression.AND([ domain, [('employee_id', 'in', self.env.context.get('active_ids', []))] ]) return { 'name': _('Time Off Analysis'), 'type': 'ir.actions.act_window', 'res_model': 'hr.leave.report', 'view_mode': 'tree,form,pivot', 'search_view_id': self.env.ref('hr_holidays.view_hr_holidays_filter_report').id, 'domain': domain, 'context': { 'search_default_group_type': True, 'search_default_year': True } } @api.model def read_group(self, domain, fields, groupby, offset=0, limit=None, orderby=False, lazy=True): if not self.user_has_groups('hr_holidays.group_hr_holidays_user') and 'name' in groupby: raise exceptions.UserError(_('Such grouping is not allowed.')) return super(LeaveReport, self).read_group(domain, fields, groupby, offset=offset, limit=limit, orderby=orderby, lazy=lazy)
class StockPicking(models.Model): _inherit = 'stock.picking' @api.depends('move_line_ids', 'move_line_ids.result_package_id') def _compute_packages(self): for package in self: packs = set() for move_line in package.move_line_ids: if move_line.result_package_id: packs.add(move_line.result_package_id.id) package.package_ids = list(packs) @api.depends('move_line_ids', 'move_line_ids.result_package_id', 'move_line_ids.product_uom_id', 'move_line_ids.qty_done') def _compute_bulk_weight(self): for picking in self: weight = 0.0 for move_line in picking.move_line_ids: if move_line.product_id and not move_line.result_package_id: weight += move_line.product_uom_id._compute_quantity(move_line.qty_done, move_line.product_id.uom_id) * move_line.product_id.weight picking.weight_bulk = weight @api.depends('package_ids', 'weight_bulk') def _compute_shipping_weight(self): for picking in self: picking.shipping_weight = picking.weight_bulk + sum([pack.shipping_weight for pack in picking.package_ids]) def _get_default_weight_uom(self): return self.env['product.template']._get_weight_uom_name_from_ir_config_parameter() def _compute_weight_uom_name(self): for package in self: package.weight_uom_name = self.env['product.template']._get_weight_uom_name_from_ir_config_parameter() carrier_price = fields.Float(string="Shipping Cost") delivery_type = fields.Selection(related='carrier_id.delivery_type', readonly=True) carrier_id = fields.Many2one("delivery.carrier", string="Carrier", check_company=True) volume = fields.Float(copy=False) weight = fields.Float(compute='_cal_weight', digits='Stock Weight', store=True, help="Total weight of the products in the picking.", compute_sudo=True) carrier_tracking_ref = fields.Char(string='Tracking Reference', copy=False) carrier_tracking_url = fields.Char(string='Tracking URL', compute='_compute_carrier_tracking_url') weight_uom_name = fields.Char(string='Weight unit of measure label', compute='_compute_weight_uom_name', readonly=True, default=_get_default_weight_uom) package_ids = fields.Many2many('stock.quant.package', compute='_compute_packages', string='Packages') weight_bulk = fields.Float('Bulk Weight', compute='_compute_bulk_weight') shipping_weight = fields.Float("Weight for Shipping", compute='_compute_shipping_weight', help="Total weight of the packages and products which are not in a package. That's the weight used to compute the cost of the shipping.") is_return_picking = fields.Boolean(compute='_compute_return_picking') return_label_ids = fields.One2many('ir.attachment', compute='_compute_return_label') @api.depends('carrier_id', 'carrier_tracking_ref') def _compute_carrier_tracking_url(self): for picking in self: picking.carrier_tracking_url = picking.carrier_id.get_tracking_link(picking) if picking.carrier_id and picking.carrier_tracking_ref else False @api.depends('carrier_id', 'move_ids_without_package') def _compute_return_picking(self): for picking in self: if picking.carrier_id and picking.carrier_id.can_generate_return: picking.is_return_picking = any(m.origin_returned_move_id for m in picking.move_ids_without_package) else: picking.is_return_picking = False def _compute_return_label(self): for picking in self: if picking.carrier_id: picking.return_label_ids = self.env['ir.attachment'].search([('res_model', '=', 'stock.picking'), ('res_id', '=', picking.id), ('name', 'like', '%s%%' % picking.carrier_id.get_return_label_prefix())]) @api.depends('move_lines') def _cal_weight(self): for picking in self: picking.weight = sum(move.weight for move in picking.move_lines if move.state != 'cancel') def _send_confirmation_email(self): for pick in self: if pick.carrier_id: if pick.carrier_id.integration_level == 'rate_and_ship' and pick.picking_type_code != 'incoming': pick.send_to_shipper() return super(StockPicking, self)._send_confirmation_email() def _pre_put_in_pack_hook(self, move_line_ids): res = super(StockPicking, self)._pre_put_in_pack_hook(move_line_ids) if not res: if self.carrier_id: return self._set_delivery_packaging() else: return res def _set_delivery_packaging(self): """ This method returns an action allowing to set the product packaging and the shipping weight on the stock.quant.package. """ self.ensure_one() view_id = self.env.ref('delivery.choose_delivery_package_view_form').id return { 'name': _('Package Details'), 'type': 'ir.actions.act_window', 'view_mode': 'form', 'res_model': 'choose.delivery.package', 'view_id': view_id, 'views': [(view_id, 'form')], 'target': 'new', 'context': dict( self.env.context, current_package_carrier_type=self.carrier_id.delivery_type, default_picking_id=self.id ), } def send_to_shipper(self): self.ensure_one() res = self.carrier_id.send_shipping(self)[0] if self.carrier_id.free_over and self.sale_id and self.sale_id._compute_amount_total_without_delivery() >= self.carrier_id.amount: res['exact_price'] = 0.0 self.carrier_price = res['exact_price'] * (1.0 + (self.carrier_id.margin / 100.0)) if res['tracking_number']: self.carrier_tracking_ref = res['tracking_number'] order_currency = self.sale_id.currency_id or self.company_id.currency_id msg = _("Shipment sent to carrier %s for shipping with tracking number %s<br/>Cost: %.2f %s") % (self.carrier_id.name, self.carrier_tracking_ref, self.carrier_price, order_currency.name) self.message_post(body=msg) self._add_delivery_cost_to_so() def print_return_label(self): self.ensure_one() res = self.carrier_id.get_return_label(self) def _add_delivery_cost_to_so(self): self.ensure_one() sale_order = self.sale_id if sale_order and self.carrier_id.invoice_policy == 'real' and self.carrier_price: delivery_lines = sale_order.order_line.filtered(lambda l: l.is_delivery and l.currency_id.is_zero(l.price_unit) and l.product_id == self.carrier_id.product_id) if not delivery_lines: sale_order._create_delivery_line(self.carrier_id, self.carrier_price) else: delivery_line = delivery_lines[0] delivery_line[0].write({ 'price_unit': self.carrier_price, # remove the estimated price from the description 'name': sale_order.carrier_id.with_context(lang=self.partner_id.lang).name, }) def open_website_url(self): self.ensure_one() if not self.carrier_tracking_url: raise UserError(_("Your delivery method has no redirect on courier provider's website to track this order.")) carrier_trackers = [] try: carrier_trackers = json.loads(self.carrier_tracking_url) except ValueError: carrier_trackers = self.carrier_tracking_url else: msg = "Tracking links for shipment: <br/>" for tracker in carrier_trackers: msg += '<a href=' + tracker[1] + '>' + tracker[0] + '</a><br/>' self.message_post(body=msg) return self.env.ref('delivery.act_delivery_trackers_url').read()[0] client_action = { 'type': 'ir.actions.act_url', 'name': "Shipment Tracking Page", 'target': 'new', 'url': self.carrier_tracking_url, } return client_action def cancel_shipment(self): for picking in self: picking.carrier_id.cancel_shipment(self) msg = "Shipment %s cancelled" % picking.carrier_tracking_ref picking.message_post(body=msg) picking.carrier_tracking_ref = False def check_packages_are_identical(self): '''Some shippers require identical packages in the same shipment. This utility checks it.''' self.ensure_one() if self.package_ids: packages = [p.packaging_id for p in self.package_ids] if len(set(packages)) != 1: package_names = ', '.join([str(p.name) for p in packages]) raise UserError(_('You are shipping different packaging types in the same shipment.\nPackaging Types: %s' % package_names)) return True
class CrmLead(models.Model): _inherit = "crm.lead" partner_latitude = fields.Float('Geo Latitude', digits=(16, 5)) partner_longitude = fields.Float('Geo Longitude', digits=(16, 5)) partner_assigned_id = fields.Many2one( 'res.partner', 'Assigned Partner', track_visibility='onchange', help="Partner this case has been forwarded/assigned to.", index=True) partner_declined_ids = fields.Many2many('res.partner', 'crm_lead_declined_partner', 'lead_id', 'partner_id', string='Partner not interested') date_assign = fields.Date( 'Partner Assignation Date', help="Last date this case was forwarded/assigned to a partner") @api.multi def _merge_data(self, fields): fields += [ 'partner_latitude', 'partner_longitude', 'partner_assigned_id', 'date_assign' ] return super(CrmLead, self)._merge_data(fields) @api.onchange("partner_assigned_id") def onchange_assign_id(self): """This function updates the "assignation date" automatically, when manually assign a partner in the geo assign tab """ partner_assigned = self.partner_assigned_id if not partner_assigned: self.date_assign = False else: self.date_assign = fields.Date.context_today(self) self.user_id = partner_assigned.user_id @api.multi def assign_salesman_of_assigned_partner(self): salesmans_leads = {} for lead in self: if (lead.stage_id.probability > 0 and lead.stage_id.probability < 100) or lead.stage_id.sequence == 1: if lead.partner_assigned_id and lead.partner_assigned_id.user_id != lead.user_id: salesmans_leads.setdefault( lead.partner_assigned_id.user_id.id, []).append(lead.id) for salesman_id, leads_ids in salesmans_leads.items(): leads = self.browse(leads_ids) leads.write({'user_id': salesman_id}) @api.multi def action_assign_partner(self): return self.assign_partner(partner_id=False) @api.multi def assign_partner(self, partner_id=False): partner_dict = {} res = False if not partner_id: partner_dict = self.search_geo_partner() for lead in self: if not partner_id: partner_id = partner_dict.get(lead.id, False) if not partner_id: tag_to_add = self.env.ref( 'website_crm_partner_assign.tag_portal_lead_partner_unavailable', False) lead.write({'tag_ids': [(4, tag_to_add.id, False)]}) continue lead.assign_geo_localize( lead.partner_latitude, lead.partner_longitude, ) partner = self.env['res.partner'].browse(partner_id) if partner.user_id: lead.allocate_salesman(partner.user_id.ids, team_id=partner.team_id.id) values = { 'date_assign': fields.Date.context_today(lead), 'partner_assigned_id': partner_id } lead.write(values) return res @api.multi def assign_geo_localize(self, latitude=False, longitude=False): if latitude and longitude: self.write({ 'partner_latitude': latitude, 'partner_longitude': longitude }) return True # Don't pass context to browse()! We need country name in english below for lead in self: if lead.partner_latitude and lead.partner_longitude: continue if lead.country_id: apikey = self.env['ir.config_parameter'].sudo().get_param( 'google.api_key_geocode') result = self.env['res.partner']._geo_localize( apikey, lead.street, lead.zip, lead.city, lead.state_id.name, lead.country_id.name) if result: lead.write({ 'partner_latitude': result[0], 'partner_longitude': result[1] }) return True @api.multi def search_geo_partner(self): Partner = self.env['res.partner'] res_partner_ids = {} self.assign_geo_localize() for lead in self: partner_ids = [] if not lead.country_id: continue latitude = lead.partner_latitude longitude = lead.partner_longitude if latitude and longitude: # 1. first way: in the same country, small area partner_ids = Partner.search([ ('partner_weight', '>', 0), ('partner_latitude', '>', latitude - 2), ('partner_latitude', '<', latitude + 2), ('partner_longitude', '>', longitude - 1.5), ('partner_longitude', '<', longitude + 1.5), ('country_id', '=', lead.country_id.id), ('id', 'not in', lead.partner_declined_ids.mapped('id')), ]) # 2. second way: in the same country, big area if not partner_ids: partner_ids = Partner.search([ ('partner_weight', '>', 0), ('partner_latitude', '>', latitude - 4), ('partner_latitude', '<', latitude + 4), ('partner_longitude', '>', longitude - 3), ('partner_longitude', '<', longitude + 3), ('country_id', '=', lead.country_id.id), ('id', 'not in', lead.partner_declined_ids.mapped('id')), ]) # 3. third way: in the same country, extra large area if not partner_ids: partner_ids = Partner.search([ ('partner_weight', '>', 0), ('partner_latitude', '>', latitude - 8), ('partner_latitude', '<', latitude + 8), ('partner_longitude', '>', longitude - 8), ('partner_longitude', '<', longitude + 8), ('country_id', '=', lead.country_id.id), ('id', 'not in', lead.partner_declined_ids.mapped('id')), ]) # 5. fifth way: anywhere in same country if not partner_ids: # still haven't found any, let's take all partners in the country! partner_ids = Partner.search([ ('partner_weight', '>', 0), ('country_id', '=', lead.country_id.id), ('id', 'not in', lead.partner_declined_ids.mapped('id')), ]) # 6. sixth way: closest partner whatsoever, just to have at least one result if not partner_ids: # warning: point() type takes (longitude, latitude) as parameters in this order! self._cr.execute( """SELECT id, distance FROM (select id, (point(partner_longitude, partner_latitude) <-> point(%s,%s)) AS distance FROM res_partner WHERE active AND partner_longitude is not null AND partner_latitude is not null AND partner_weight > 0 AND id not in (select partner_id from crm_lead_declined_partner where lead_id = %s) ) AS d ORDER BY distance LIMIT 1""", (longitude, latitude, lead.id)) res = self._cr.dictfetchone() if res: partner_ids = Partner.browse([res['id']]) total_weight = 0 toassign = [] for partner in partner_ids: total_weight += partner.partner_weight toassign.append((partner.id, total_weight)) random.shuffle( toassign ) # avoid always giving the leads to the first ones in db natural order! nearest_weight = random.randint(0, total_weight) for partner_id, weight in toassign: if nearest_weight <= weight: res_partner_ids[lead.id] = partner_id break return res_partner_ids @api.multi def partner_interested(self, comment=False): message = _('<p>I am interested by this lead.</p>') if comment: message += '<p>%s</p>' % comment for lead in self: lead.message_post(body=message, subtype="mail.mt_note") lead.sudo().convert_opportunity( lead.partner_id.id) # sudo required to convert partner data @api.multi def partner_desinterested(self, comment=False, contacted=False, spam=False): if contacted: message = '<p>%s</p>' % _( 'I am not interested by this lead. I contacted the lead.') else: message = '<p>%s</p>' % _( 'I am not interested by this lead. I have not contacted the lead.' ) partner_ids = self.env['res.partner'].search([ ('id', 'child_of', self.env.user.partner_id.commercial_partner_id.id) ]) self.message_unsubscribe(partner_ids=partner_ids.ids) if comment: message += '<p>%s</p>' % comment self.message_post(body=message, subtype="mail.mt_note") values = { 'partner_assigned_id': False, } if spam: tag_spam = self.env.ref( 'website_crm_partner_assign.tag_portal_lead_is_spam', False) if tag_spam and tag_spam not in self.tag_ids: values['tag_ids'] = [(4, tag_spam.id, False)] if partner_ids: values['partner_declined_ids'] = [(4, p, 0) for p in partner_ids.ids] self.sudo().write(values) @api.multi def update_lead_portal(self, values): self.check_access_rights('write') for lead in self: lead_values = { 'planned_revenue': values['planned_revenue'], 'probability': values['probability'], 'priority': values['priority'], 'date_deadline': values['date_deadline'] or False, } # As activities may belong to several users, only the current portal user activity # will be modified by the portal form. If no activity exist we create a new one instead # that we assign to the portal user. user_activity = lead.sudo().activity_ids.filtered( lambda activity: activity.user_id == self.env.user)[:1] if values['activity_date_deadline']: if user_activity: user_activity.sudo().write({ 'activity_type_id': values['activity_type_id'], 'summary': values['activity_summary'], 'date_deadline': values['activity_date_deadline'], }) else: self.env['mail.activity'].sudo().create({ 'res_model_id': self.env.ref('crm.model_crm_lead').id, 'res_id': lead.id, 'user_id': self.env.user.id, 'activity_type_id': values['activity_type_id'], 'summary': values['activity_summary'], 'date_deadline': values['activity_date_deadline'], }) lead.write(lead_values) @api.model def create_opp_portal(self, values): if not (self.env.user.partner_id.grade_id or self.env.user.commercial_partner_id.grade_id): raise AccessDenied() user = self.env.user self = self.sudo() if not (values['contact_name'] and values['description'] and values['title']): return {'errors': _('All fields are required !')} tag_own = self.env.ref( 'website_crm_partner_assign.tag_portal_lead_own_opp', False) values = { 'contact_name': values['contact_name'], 'name': values['title'], 'description': values['description'], 'priority': '2', 'partner_assigned_id': user.commercial_partner_id.id, } if tag_own: values['tag_ids'] = [(4, tag_own.id, False)] lead = self.create(values) lead.assign_salesman_of_assigned_partner() lead.convert_opportunity(lead.partner_id.id) return {'id': lead.id} # # DO NOT FORWARD PORT IN MASTER # instead, crm.lead should implement portal.mixin # @api.multi def get_access_action(self, access_uid=None): """ Instead of the classic form view, redirect to the online document for portal users or if force_website=True in the context. """ self.ensure_one() user, record = self.env.user, self if access_uid: try: record.check_access_rights('read') record.check_access_rule("read") except AccessError: return super(CrmLead, self).get_access_action(access_uid) user = self.env['res.users'].sudo().browse(access_uid) record = self.sudo(user) if user.share or self.env.context.get('force_website'): try: record.check_access_rights('read') record.check_access_rule('read') except AccessError: pass else: return { 'type': 'ir.actions.act_url', 'url': '/my/opportunity/%s' % record.id, } return super(CrmLead, self).get_access_action(access_uid)
class LandedCost(models.Model): _name = 'stock.landed.cost' _description = 'Stock Landed Cost' _inherit = ['mail.thread', 'mail.activity.mixin'] def _default_account_journal_id(self): """Take the journal configured in the company, else fallback on the stock journal.""" lc_journal = self.env['account.journal'] if self.env.company.lc_journal_id: lc_journal = self.env.company.lc_journal_id else: ir_property = self.env['ir.property'].search([ ('name', '=', 'property_stock_journal'), ('company_id', '=', self.env.company.id) ], limit=1) if ir_property: lc_journal = ir_property.get_by_record() return lc_journal name = fields.Char( 'Name', default=lambda self: _('New'), copy=False, readonly=True, tracking=True) date = fields.Date( 'Date', default=fields.Date.context_today, copy=False, required=True, states={'done': [('readonly', True)]}, tracking=True) picking_ids = fields.Many2many( 'stock.picking', string='Transfers', copy=False, states={'done': [('readonly', True)]}) cost_lines = fields.One2many( 'stock.landed.cost.lines', 'cost_id', 'Cost Lines', copy=True, states={'done': [('readonly', True)]}) valuation_adjustment_lines = fields.One2many( 'stock.valuation.adjustment.lines', 'cost_id', 'Valuation Adjustments', states={'done': [('readonly', True)]}) description = fields.Text( 'Item Description', states={'done': [('readonly', True)]}) amount_total = fields.Float( 'Total', compute='_compute_total_amount', digits=0, store=True, tracking=True) state = fields.Selection([ ('draft', 'Draft'), ('done', 'Posted'), ('cancel', 'Cancelled')], 'State', default='draft', copy=False, readonly=True, tracking=True) account_move_id = fields.Many2one( 'account.move', 'Journal Entry', copy=False, readonly=True) account_journal_id = fields.Many2one( 'account.journal', 'Account Journal', required=True, states={'done': [('readonly', True)]}, default=lambda self: self._default_account_journal_id()) company_id = fields.Many2one('res.company', string="Company", related='account_journal_id.company_id') stock_valuation_layer_ids = fields.One2many('stock.valuation.layer', 'stock_landed_cost_id') vendor_bill_id = fields.Many2one( 'account.move', 'Vendor Bill', copy=False, domain=[('type', '=', 'in_invoice')]) currency_id = fields.Many2one('res.currency', related='company_id.currency_id') @api.depends('cost_lines.price_unit') def _compute_total_amount(self): for cost in self: cost.amount_total = sum(line.price_unit for line in cost.cost_lines) @api.model def create(self, vals): if vals.get('name', _('New')) == _('New'): vals['name'] = self.env['ir.sequence'].next_by_code('stock.landed.cost') return super(LandedCost, self).create(vals) def unlink(self): self.button_cancel() return super(LandedCost, self).unlink() def _track_subtype(self, init_values): if 'state' in init_values and self.state == 'done': return self.env.ref('stock_landed_costs.mt_stock_landed_cost_open') return super(LandedCost, self)._track_subtype(init_values) def button_cancel(self): if any(cost.state == 'done' for cost in self): raise UserError( _('Validated landed costs cannot be cancelled, but you could create negative landed costs to reverse them')) return self.write({'state': 'cancel'}) def button_validate(self): if any(cost.state != 'draft' for cost in self): raise UserError(_('Only draft landed costs can be validated')) if not all(cost.picking_ids for cost in self): raise UserError(_('Please define the transfers on which those additional costs should apply.')) cost_without_adjusment_lines = self.filtered(lambda c: not c.valuation_adjustment_lines) if cost_without_adjusment_lines: cost_without_adjusment_lines.compute_landed_cost() if not self._check_sum(): raise UserError(_('Cost and adjustments lines do not match. You should maybe recompute the landed costs.')) for cost in self: move = self.env['account.move'] move_vals = { 'journal_id': cost.account_journal_id.id, 'date': cost.date, 'ref': cost.name, 'line_ids': [], 'type': 'entry', } for line in cost.valuation_adjustment_lines.filtered(lambda line: line.move_id): remaining_qty = sum(line.move_id.stock_valuation_layer_ids.mapped('remaining_qty')) linked_layer = line.move_id.stock_valuation_layer_ids[-1] # Maybe the LC layer should be linked to multiple IN layer? # Prorate the value at what's still in stock cost_to_add = (remaining_qty / line.move_id.product_qty) * line.additional_landed_cost if not cost.company_id.currency_id.is_zero(cost_to_add): valuation_layer = self.env['stock.valuation.layer'].create({ 'value': cost_to_add, 'unit_cost': 0, 'quantity': 0, 'remaining_qty': 0, 'stock_valuation_layer_id': linked_layer.id, 'description': cost.name, 'stock_move_id': line.move_id.id, 'product_id': line.move_id.product_id.id, 'stock_landed_cost_id': cost.id, 'company_id': cost.company_id.id, }) move_vals['stock_valuation_layer_ids'] = [(6, None, [valuation_layer.id])] linked_layer.remaining_value += cost_to_add # Update the AVCO product = line.move_id.product_id if product.cost_method == 'average' and not float_is_zero(product.quantity_svl, precision_rounding=product.uom_id.rounding): product.with_context(force_company=self.company_id.id).sudo().standard_price += cost_to_add / product.quantity_svl # `remaining_qty` is negative if the move is out and delivered proudcts that were not # in stock. qty_out = 0 if line.move_id._is_in(): qty_out = line.move_id.product_qty - remaining_qty elif line.move_id._is_out(): qty_out = line.move_id.product_qty move_vals['line_ids'] += line._create_accounting_entries(move, qty_out) move = move.create(move_vals) cost.write({'state': 'done', 'account_move_id': move.id}) move.post() if cost.vendor_bill_id and cost.vendor_bill_id.state == 'posted' and cost.company_id.anglo_saxon_accounting: all_amls = cost.vendor_bill_id.line_ids | cost.account_move_id.line_ids for product in cost.cost_lines.product_id: accounts = product.product_tmpl_id.get_product_accounts() input_account = accounts['stock_input'] all_amls.filtered(lambda aml: aml.account_id == input_account).reconcile() return True def _check_sum(self): """ Check if each cost line its valuation lines sum to the correct amount and if the overall total amount is correct also """ prec_digits = self.env.company.currency_id.decimal_places for landed_cost in self: total_amount = sum(landed_cost.valuation_adjustment_lines.mapped('additional_landed_cost')) if not tools.float_is_zero(total_amount - landed_cost.amount_total, precision_digits=prec_digits): return False val_to_cost_lines = defaultdict(lambda: 0.0) for val_line in landed_cost.valuation_adjustment_lines: val_to_cost_lines[val_line.cost_line_id] += val_line.additional_landed_cost if any(not tools.float_is_zero(cost_line.price_unit - val_amount, precision_digits=prec_digits) for cost_line, val_amount in val_to_cost_lines.items()): return False return True def get_valuation_lines(self): lines = [] for move in self.mapped('picking_ids').mapped('move_lines'): # it doesn't make sense to make a landed cost for a product that isn't set as being valuated in real time at real cost if move.product_id.valuation != 'real_time' or move.product_id.cost_method not in ('fifo', 'average'): continue vals = { 'product_id': move.product_id.id, 'move_id': move.id, 'quantity': move.product_qty, 'former_cost': sum(move.stock_valuation_layer_ids.mapped('value')), 'weight': move.product_id.weight * move.product_qty, 'volume': move.product_id.volume * move.product_qty } lines.append(vals) if not lines and self.mapped('picking_ids'): raise UserError(_("You cannot apply landed costs on the chosen transfer(s). Landed costs can only be applied for products with automated inventory valuation and FIFO or average costing method.")) return lines def compute_landed_cost(self): AdjustementLines = self.env['stock.valuation.adjustment.lines'] AdjustementLines.search([('cost_id', 'in', self.ids)]).unlink() digits = self.env['decimal.precision'].precision_get('Product Price') towrite_dict = {} for cost in self.filtered(lambda cost: cost.picking_ids): total_qty = 0.0 total_cost = 0.0 total_weight = 0.0 total_volume = 0.0 total_line = 0.0 all_val_line_values = cost.get_valuation_lines() for val_line_values in all_val_line_values: for cost_line in cost.cost_lines: val_line_values.update({'cost_id': cost.id, 'cost_line_id': cost_line.id}) self.env['stock.valuation.adjustment.lines'].create(val_line_values) total_qty += val_line_values.get('quantity', 0.0) total_weight += val_line_values.get('weight', 0.0) total_volume += val_line_values.get('volume', 0.0) former_cost = val_line_values.get('former_cost', 0.0) # round this because former_cost on the valuation lines is also rounded total_cost += tools.float_round(former_cost, precision_digits=digits) if digits else former_cost total_line += 1 for line in cost.cost_lines: value_split = 0.0 for valuation in cost.valuation_adjustment_lines: value = 0.0 if valuation.cost_line_id and valuation.cost_line_id.id == line.id: if line.split_method == 'by_quantity' and total_qty: per_unit = (line.price_unit / total_qty) value = valuation.quantity * per_unit elif line.split_method == 'by_weight' and total_weight: per_unit = (line.price_unit / total_weight) value = valuation.weight * per_unit elif line.split_method == 'by_volume' and total_volume: per_unit = (line.price_unit / total_volume) value = valuation.volume * per_unit elif line.split_method == 'equal': value = (line.price_unit / total_line) elif line.split_method == 'by_current_cost_price' and total_cost: per_unit = (line.price_unit / total_cost) value = valuation.former_cost * per_unit else: value = (line.price_unit / total_line) if digits: value = tools.float_round(value, precision_digits=digits, rounding_method='UP') fnc = min if line.price_unit > 0 else max value = fnc(value, line.price_unit - value_split) value_split += value if valuation.id not in towrite_dict: towrite_dict[valuation.id] = value else: towrite_dict[valuation.id] += value for key, value in towrite_dict.items(): AdjustementLines.browse(key).write({'additional_landed_cost': value}) return True def action_view_stock_valuation_layers(self): self.ensure_one() domain = [('id', 'in', self.stock_valuation_layer_ids.ids)] action = self.env.ref('stock_account.stock_valuation_layer_action').read()[0] return dict(action, domain=domain)
class AdjustmentLines(models.Model): _name = 'stock.valuation.adjustment.lines' _description = 'Valuation Adjustment Lines' name = fields.Char( 'Description', compute='_compute_name', store=True) cost_id = fields.Many2one( 'stock.landed.cost', 'Landed Cost', ondelete='cascade', required=True) cost_line_id = fields.Many2one( 'stock.landed.cost.lines', 'Cost Line', readonly=True) move_id = fields.Many2one('stock.move', 'Stock Move', readonly=True) product_id = fields.Many2one('product.product', 'Product', required=True) quantity = fields.Float( 'Quantity', default=1.0, digits=0, required=True) weight = fields.Float( 'Weight', default=1.0, digits='Stock Weight') volume = fields.Float( 'Volume', default=1.0) former_cost = fields.Float( 'Original Value', digits='Product Price') additional_landed_cost = fields.Float( 'Additional Landed Cost', digits='Product Price') final_cost = fields.Float( 'New Value', compute='_compute_final_cost', digits=0, store=True) currency_id = fields.Many2one('res.currency', related='cost_id.company_id.currency_id') @api.depends('cost_line_id.name', 'product_id.code', 'product_id.name') def _compute_name(self): for line in self: name = '%s - ' % (line.cost_line_id.name if line.cost_line_id else '') line.name = name + (line.product_id.code or line.product_id.name or '') @api.depends('former_cost', 'additional_landed_cost') def _compute_final_cost(self): for line in self: line.final_cost = line.former_cost + line.additional_landed_cost def _create_accounting_entries(self, move, qty_out): # TDE CLEANME: product chosen for computation ? cost_product = self.cost_line_id.product_id if not cost_product: return False accounts = self.product_id.product_tmpl_id.get_product_accounts() debit_account_id = accounts.get('stock_valuation') and accounts['stock_valuation'].id or False # If the stock move is dropshipped move we need to get the cost account instead the stock valuation account if self.move_id._is_dropshipped(): debit_account_id = accounts.get('expense') and accounts['expense'].id or False already_out_account_id = accounts['stock_output'].id credit_account_id = self.cost_line_id.account_id.id or cost_product.categ_id.property_stock_account_input_categ_id.id if not credit_account_id: raise UserError(_('Please configure Stock Expense Account for product: %s.') % (cost_product.name)) return self._create_account_move_line(move, credit_account_id, debit_account_id, qty_out, already_out_account_id) def _create_account_move_line(self, move, credit_account_id, debit_account_id, qty_out, already_out_account_id): """ Generate the account.move.line values to track the landed cost. Afterwards, for the goods that are already out of stock, we should create the out moves """ AccountMoveLine = [] base_line = { 'name': self.name, 'product_id': self.product_id.id, 'quantity': 0, } debit_line = dict(base_line, account_id=debit_account_id) credit_line = dict(base_line, account_id=credit_account_id) diff = self.additional_landed_cost if diff > 0: debit_line['debit'] = diff credit_line['credit'] = diff else: # negative cost, reverse the entry debit_line['credit'] = -diff credit_line['debit'] = -diff AccountMoveLine.append([0, 0, debit_line]) AccountMoveLine.append([0, 0, credit_line]) # Create account move lines for quants already out of stock if qty_out > 0: debit_line = dict(base_line, name=(self.name + ": " + str(qty_out) + _(' already out')), quantity=0, account_id=already_out_account_id) credit_line = dict(base_line, name=(self.name + ": " + str(qty_out) + _(' already out')), quantity=0, account_id=debit_account_id) diff = diff * qty_out / self.quantity if diff > 0: debit_line['debit'] = diff credit_line['credit'] = diff else: # negative cost, reverse the entry debit_line['credit'] = -diff credit_line['debit'] = -diff AccountMoveLine.append([0, 0, debit_line]) AccountMoveLine.append([0, 0, credit_line]) if self.env.company.anglo_saxon_accounting: expense_account_id = self.product_id.product_tmpl_id.get_product_accounts()['expense'].id debit_line = dict(base_line, name=(self.name + ": " + str(qty_out) + _(' already out')), quantity=0, account_id=expense_account_id) credit_line = dict(base_line, name=(self.name + ": " + str(qty_out) + _(' already out')), quantity=0, account_id=already_out_account_id) if diff > 0: debit_line['debit'] = diff credit_line['credit'] = diff else: # negative cost, reverse the entry debit_line['credit'] = -diff credit_line['debit'] = -diff AccountMoveLine.append([0, 0, debit_line]) AccountMoveLine.append([0, 0, credit_line]) return AccountMoveLine
class MrpAbstractWorkorder(models.AbstractModel): _name = "mrp.abstract.workorder" _description = "Common code between produce wizards and workorders." _check_company_auto = True production_id = fields.Many2one('mrp.production', 'Manufacturing Order', required=True, check_company=True) product_id = fields.Many2one(related='production_id.product_id', readonly=True, store=True, check_company=True) qty_producing = fields.Float(string='Currently Produced Quantity', digits='Product Unit of Measure') product_uom_id = fields.Many2one('uom.uom', 'Unit of Measure', required=True, readonly=True) finished_lot_id = fields.Many2one( 'stock.production.lot', string='Lot/Serial Number', domain= "[('product_id', '=', product_id), ('company_id', '=', company_id)]", check_company=True) product_tracking = fields.Selection(related="product_id.tracking") consumption = fields.Selection( [('strict', 'Strict'), ('flexible', 'Flexible')], required=True, ) use_create_components_lots = fields.Boolean( related="production_id.picking_type_id.use_create_components_lots") company_id = fields.Many2one(related='production_id.company_id') @api.model def _prepare_component_quantity(self, move, qty_producing): """ helper that computes quantity to consume (or to create in case of byproduct) depending on the quantity producing and the move's unit factor""" if move.product_id.tracking == 'serial': uom = move.product_id.uom_id else: uom = move.product_uom return move.product_uom._compute_quantity(qty_producing * move.unit_factor, uom, round=False) def _workorder_line_ids(self): self.ensure_one() return self.raw_workorder_line_ids | self.finished_workorder_line_ids @api.onchange('qty_producing') def _onchange_qty_producing(self): """ Modify the qty currently producing will modify the existing workorder line in order to match the new quantity to consume for each component and their reserved quantity. """ if self.qty_producing <= 0: raise UserError( _('You have to produce at least one %s.') % self.product_uom_id.name) line_values = self._update_workorder_lines() for values in line_values['to_create']: self.env[self._workorder_line_ids()._name].new(values) for line in line_values['to_delete']: if line in self.raw_workorder_line_ids: self.raw_workorder_line_ids -= line else: self.finished_workorder_line_ids -= line for line, vals in line_values['to_update'].items(): line.update(vals) def _update_workorder_lines(self): """ Update workorder lines, according to the new qty currently produced. It returns a dict with line to create, update or delete. It do not directly write or unlink the line because this function is used in onchange and request that write on db (e.g. workorder creation). """ line_values = {'to_create': [], 'to_delete': [], 'to_update': {}} # moves are actual records move_finished_ids = self.move_finished_ids._origin.filtered( lambda move: move.product_id != self.product_id and move.state not in ('done', 'cancel')) move_raw_ids = self.move_raw_ids._origin.filtered( lambda move: move.state not in ('done', 'cancel')) for move in move_raw_ids | move_finished_ids: move_workorder_lines = self._workorder_line_ids().filtered( lambda w: w.move_id == move) # Compute the new quantity for the current component rounding = move.product_uom.rounding new_qty = self._prepare_component_quantity(move, self.qty_producing) # In case the production uom is different than the workorder uom # it means the product is serial and production uom is not the reference new_qty = self.product_uom_id._compute_quantity( new_qty, self.production_id.product_uom_id, round=False) qty_todo = float_round( new_qty - sum(move_workorder_lines.mapped('qty_to_consume')), precision_rounding=rounding) # Remove or lower quantity on exisiting workorder lines if float_compare(qty_todo, 0.0, precision_rounding=rounding) < 0: qty_todo = abs(qty_todo) # Try to decrease or remove lines that are not reserved and # partialy reserved first. A different decrease strategy could # be define in _unreserve_order method. for workorder_line in move_workorder_lines.sorted( key=lambda wl: wl._unreserve_order()): if float_compare(qty_todo, 0, precision_rounding=rounding) <= 0: break # If the quantity to consume on the line is lower than the # quantity to remove, the line could be remove. if float_compare(workorder_line.qty_to_consume, qty_todo, precision_rounding=rounding) <= 0: qty_todo = float_round(qty_todo - workorder_line.qty_to_consume, precision_rounding=rounding) if line_values['to_delete']: line_values['to_delete'] |= workorder_line else: line_values['to_delete'] = workorder_line # decrease the quantity on the line else: new_val = workorder_line.qty_to_consume - qty_todo # avoid to write a negative reserved quantity new_reserved = max( 0, workorder_line.qty_reserved - qty_todo) line_values['to_update'][workorder_line] = { 'qty_to_consume': new_val, 'qty_done': new_val, 'qty_reserved': new_reserved, } qty_todo = 0 else: # Search among wo lines which one could be updated qty_reserved_wl = defaultdict(float) # Try to update the line with the greater reservation first in # order to promote bigger batch. for workorder_line in move_workorder_lines.sorted( key=lambda wl: wl.qty_reserved, reverse=True): rounding = workorder_line.product_uom_id.rounding if float_compare(qty_todo, 0, precision_rounding=rounding) <= 0: break move_lines = workorder_line._get_move_lines() qty_reserved_wl[ workorder_line.lot_id] += workorder_line.qty_reserved # The reserved quantity according to exisiting move line # already produced (with qty_done set) and other production # lines with the same lot that are currently on production. qty_reserved_remaining = sum( move_lines.mapped('product_uom_qty')) - sum( move_lines.mapped('qty_done')) - qty_reserved_wl[ workorder_line.lot_id] if float_compare(qty_reserved_remaining, 0, precision_rounding=rounding) > 0: qty_to_add = min(qty_reserved_remaining, qty_todo) line_values['to_update'][workorder_line] = { 'qty_done': workorder_line.qty_to_consume + qty_to_add, 'qty_to_consume': workorder_line.qty_to_consume + qty_to_add, 'qty_reserved': workorder_line.qty_reserved + qty_to_add, } qty_todo -= qty_to_add qty_reserved_wl[workorder_line.lot_id] += qty_to_add # If a line exists without reservation and without lot. It # means that previous operations could not find any reserved # quantity and created a line without lot prefilled. In this # case, the system will not find an existing move line with # available reservation anymore and will increase this line # instead of creating a new line without lot and reserved # quantities. if not workorder_line.qty_reserved and not workorder_line.lot_id and workorder_line.product_tracking != 'serial': line_values['to_update'][workorder_line] = { 'qty_done': workorder_line.qty_to_consume + qty_todo, 'qty_to_consume': workorder_line.qty_to_consume + qty_todo, } qty_todo = 0 # if there are still qty_todo, create new wo lines if float_compare(qty_todo, 0.0, precision_rounding=rounding) > 0: for values in self._generate_lines_values(move, qty_todo): line_values['to_create'].append(values) return line_values @api.model def _generate_lines_values(self, move, qty_to_consume): """ Create workorder line. First generate line based on the reservation, in order to prefill reserved quantity, lot and serial number. If the quantity to consume is greater than the reservation quantity then create line with the correct quantity to consume but without lot or serial number. """ lines = [] is_tracked = move.product_id.tracking != 'none' if move in self.move_raw_ids._origin: # Get the inverse_name (many2one on line) of raw_workorder_line_ids initial_line_values = { self.raw_workorder_line_ids._get_raw_workorder_inverse_name(): self.id } else: # Get the inverse_name (many2one on line) of finished_workorder_line_ids initial_line_values = { self.finished_workorder_line_ids._get_finished_workoder_inverse_name( ): self.id } for move_line in move.move_line_ids: line = dict(initial_line_values) if float_compare( qty_to_consume, 0.0, precision_rounding=move.product_uom.rounding) <= 0: break # move line already 'used' in workorder (from its lot for instance) if move_line.lot_produced_ids or float_compare( move_line.product_uom_qty, move_line.qty_done, precision_rounding=move.product_uom.rounding) <= 0: continue # search wo line on which the lot is not fully consumed or other reserved lot linked_wo_line = self._workorder_line_ids().filtered( lambda line: line.move_id == move and line.lot_id == move_line. lot_id) if linked_wo_line: if float_compare( sum(linked_wo_line.mapped('qty_to_consume')), move_line.product_uom_qty - move_line.qty_done, precision_rounding=move.product_uom.rounding) < 0: to_consume_in_line = min( qty_to_consume, move_line.product_uom_qty - move_line.qty_done - sum(linked_wo_line.mapped('qty_to_consume'))) else: continue else: to_consume_in_line = min( qty_to_consume, move_line.product_uom_qty - move_line.qty_done) line.update({ 'move_id': move.id, 'product_id': move.product_id.id, 'product_uom_id': is_tracked and move.product_id.uom_id.id or move.product_uom.id, 'qty_to_consume': to_consume_in_line, 'qty_reserved': to_consume_in_line, 'lot_id': move_line.lot_id.id, 'qty_done': to_consume_in_line, }) lines.append(line) qty_to_consume -= to_consume_in_line # The move has not reserved the whole quantity so we create new wo lines if float_compare(qty_to_consume, 0.0, precision_rounding=move.product_uom.rounding) > 0: line = dict(initial_line_values) if move.product_id.tracking == 'serial': while float_compare( qty_to_consume, 0.0, precision_rounding=move.product_uom.rounding) > 0: line.update({ 'move_id': move.id, 'product_id': move.product_id.id, 'product_uom_id': move.product_id.uom_id.id, 'qty_to_consume': 1, 'qty_done': 1, }) lines.append(line) qty_to_consume -= 1 else: line.update({ 'move_id': move.id, 'product_id': move.product_id.id, 'product_uom_id': move.product_uom.id, 'qty_to_consume': qty_to_consume, 'qty_done': qty_to_consume, }) lines.append(line) return lines def _update_finished_move(self): """ Update the finished move & move lines in order to set the finished product lot on it as well as the produced quantity. This method get the information either from the last workorder or from the Produce wizard.""" production_move = self.production_id.move_finished_ids.filtered( lambda move: move.product_id == self.product_id and move.state not in ('done', 'cancel')) if production_move and production_move.product_id.tracking != 'none': if not self.finished_lot_id: raise UserError( _('You need to provide a lot for the finished product.')) move_line = production_move.move_line_ids.filtered( lambda line: line.lot_id.id == self.finished_lot_id.id) if move_line: if self.product_id.tracking == 'serial': raise UserError( _('You cannot produce the same serial number twice.')) move_line.product_uom_qty += self.qty_producing move_line.qty_done += self.qty_producing else: location_dest_id = production_move.location_dest_id._get_putaway_strategy( self.product_id).id or production_move.location_dest_id.id move_line.create({ 'move_id': production_move.id, 'product_id': production_move.product_id.id, 'lot_id': self.finished_lot_id.id, 'product_uom_qty': self.qty_producing, 'product_uom_id': self.product_uom_id.id, 'qty_done': self.qty_producing, 'location_id': production_move.location_id.id, 'location_dest_id': location_dest_id, }) else: rounding = production_move.product_uom.rounding production_move._set_quantity_done( float_round(self.qty_producing, precision_rounding=rounding)) def _update_moves(self): """ Once the production is done. Modify the workorder lines into stock move line with the registered lot and quantity done. """ # Before writting produce quantities, we ensure they respect the bom strictness self._strict_consumption_check() vals_list = [] workorder_lines_to_process = self._workorder_line_ids().filtered( lambda line: line.product_id != self.product_id and line.qty_done > 0) for line in workorder_lines_to_process: line._update_move_lines() if float_compare( line.qty_done, 0, precision_rounding=line.product_uom_id.rounding) > 0: vals_list += line._create_extra_move_lines() self._workorder_line_ids().filtered( lambda line: line.product_id != self.product_id).unlink() self.env['stock.move.line'].create(vals_list) def _strict_consumption_check(self): if self.consumption == 'strict': for move in self.move_raw_ids: lines = self._workorder_line_ids().filtered( lambda l: l.move_id == move) qty_done = sum(lines.mapped('qty_done')) qty_to_consume = sum(lines.mapped('qty_to_consume')) rounding = self.product_uom_id.rounding if float_compare(qty_done, qty_to_consume, precision_rounding=rounding) != 0: raise UserError( _('You should consume the quantity of %s defined in the BoM. If you want to consume more or less components, change the consumption setting on the BoM.' ) % lines[0].product_id.name)
class MrpAbstractWorkorderLine(models.AbstractModel): _name = "mrp.abstract.workorder.line" _description = "Abstract model to implement product_produce_line as well as\ workorder_line" _check_company_auto = True move_id = fields.Many2one('stock.move', check_company=True) product_id = fields.Many2one('product.product', 'Product', required=True, check_company=True) product_tracking = fields.Selection(related="product_id.tracking") lot_id = fields.Many2one( 'stock.production.lot', 'Lot/Serial Number', check_company=True, domain= "[('product_id', '=', product_id), '|', ('company_id', '=', False), ('company_id', '=', company_id)]" ) qty_to_consume = fields.Float('To Consume', digits='Product Unit of Measure') product_uom_id = fields.Many2one('uom.uom', string='Unit of Measure') qty_done = fields.Float('Consumed', digits='Product Unit of Measure') qty_reserved = fields.Float('Reserved', digits='Product Unit of Measure') company_id = fields.Many2one('res.company', compute='_compute_company_id') @api.onchange('lot_id') def _onchange_lot_id(self): """ When the user is encoding a produce line for a tracked product, we apply some logic to help him. This onchange will automatically switch `qty_done` to 1.0. """ if self.product_id.tracking == 'serial': self.qty_done = 1 @api.onchange('product_id') def _onchange_product_id(self): if self.product_id and not self.move_id: self.product_uom_id = self.product_id.uom_id @api.onchange('qty_done') def _onchange_qty_done(self): """ When the user is encoding a produce line for a tracked product, we apply some logic to help him. This onchange will warn him if he set `qty_done` to a non-supported value. """ res = {} if self.product_id.tracking == 'serial' and not float_is_zero( self.qty_done, self.product_uom_id.rounding): if float_compare( self.qty_done, 1.0, precision_rounding=self.product_uom_id.rounding) != 0: message = _( 'You can only process 1.0 %s of products with unique serial number.' ) % self.product_id.uom_id.name res['warning'] = {'title': _('Warning'), 'message': message} return res def _compute_company_id(self): for line in self: line.company_id = line._get_production().company_id def _update_move_lines(self): """ update a move line to save the workorder line data""" self.ensure_one() if self.lot_id: move_lines = self.move_id.move_line_ids.filtered( lambda ml: ml.lot_id == self.lot_id and not ml.lot_produced_ids ) else: move_lines = self.move_id.move_line_ids.filtered( lambda ml: not ml.lot_id and not ml.lot_produced_ids) # Sanity check: if the product is a serial number and `lot` is already present in the other # consumed move lines, raise. if self.product_id.tracking != 'none' and not self.lot_id: raise UserError( _('Please enter a lot or serial number for %s !' % self.product_id.display_name)) if self.lot_id and self.product_id.tracking == 'serial' and self.lot_id in self.move_id.move_line_ids.filtered( lambda ml: ml.qty_done).mapped('lot_id'): raise UserError( _('You cannot consume the same serial number twice. Please correct the serial numbers encoded.' )) # Update reservation and quantity done for ml in move_lines: rounding = ml.product_uom_id.rounding if float_compare(self.qty_done, 0, precision_rounding=rounding) <= 0: break quantity_to_process = min(self.qty_done, ml.product_uom_qty - ml.qty_done) self.qty_done -= quantity_to_process new_quantity_done = (ml.qty_done + quantity_to_process) # if we produce less than the reserved quantity to produce the finished products # in different lots, # we create different component_move_lines to record which one was used # on which lot of finished product if float_compare(new_quantity_done, ml.product_uom_qty, precision_rounding=rounding) >= 0: ml.write({ 'qty_done': new_quantity_done, 'lot_produced_ids': self._get_produced_lots(), }) else: new_qty_reserved = ml.product_uom_qty - new_quantity_done default = { 'product_uom_qty': new_quantity_done, 'qty_done': new_quantity_done, 'lot_produced_ids': self._get_produced_lots(), } ml.copy(default=default) ml.with_context(bypass_reservation_update=True).write({ 'product_uom_qty': new_qty_reserved, 'qty_done': 0 }) def _create_extra_move_lines(self): """Create new sml if quantity produced is bigger than the reserved one""" vals_list = [] quants = self.env['stock.quant']._gather(self.product_id, self.move_id.location_id, lot_id=self.lot_id, strict=False) # Search for a sub-locations where the product is available. # Loop on the quants to get the locations. If there is not enough # quantity into stock, we take the move location. Anyway, no # reservation is made, so it is still possible to change it afterwards. for quant in quants: quantity = quant.quantity - quant.reserved_quantity quantity = self.product_id.uom_id._compute_quantity( quantity, self.product_uom_id, rounding_method='HALF-UP') rounding = quant.product_uom_id.rounding if (float_compare(quant.quantity, 0, precision_rounding=rounding) <= 0 or float_compare( quantity, 0, precision_rounding=self.product_uom_id.rounding) <= 0): continue vals = { 'move_id': self.move_id.id, 'product_id': self.product_id.id, 'location_id': quant.location_id.id, 'location_dest_id': self.move_id.location_dest_id.id, 'product_uom_qty': 0, 'product_uom_id': self.product_uom_id.id, 'qty_done': min(quantity, self.qty_done), 'lot_produced_ids': self._get_produced_lots(), } if self.lot_id: vals.update({'lot_id': self.lot_id.id}) vals_list.append(vals) self.qty_done -= vals['qty_done'] # If all the qty_done is distributed, we can close the loop if float_compare( self.qty_done, 0, precision_rounding=self.product_id.uom_id.rounding) <= 0: break if float_compare( self.qty_done, 0, precision_rounding=self.product_id.uom_id.rounding) > 0: vals = { 'move_id': self.move_id.id, 'product_id': self.product_id.id, 'location_id': self.move_id.location_id.id, 'location_dest_id': self.move_id.location_dest_id.id, 'product_uom_qty': 0, 'product_uom_id': self.product_uom_id.id, 'qty_done': self.qty_done, 'lot_produced_ids': self._get_produced_lots(), } if self.lot_id: vals.update({'lot_id': self.lot_id.id}) vals_list.append(vals) return vals_list def _unreserve_order(self): """ Unreserve line with lower reserved quantity first """ self.ensure_one() return (self.qty_reserved, ) def _get_move_lines(self): return self.move_id.move_line_ids.filtered( lambda ml: ml.lot_id == self.lot_id and ml.product_id == self. product_id) def _get_produced_lots(self): return self.move_id in self._get_production( ).move_raw_ids and self._get_final_lots() and [ (4, lot.id) for lot in self._get_final_lots() ] @api.model def _get_raw_workorder_inverse_name(self): raise NotImplementedError( 'Method _get_raw_workorder_inverse_name() undefined on %s' % self) @api.model def _get_finished_workoder_inverse_name(self): raise NotImplementedError( 'Method _get_finished_workoder_inverse_name() undefined on %s' % self) # To be implemented in specific model def _get_final_lots(self): raise NotImplementedError('Method _get_final_lots() undefined on %s' % self) def _get_production(self): raise NotImplementedError('Method _get_production() undefined on %s' % self)
class ContractCreation(models.TransientModel): _name = "saas.contract.creation" _description = 'Contract Creation Wizard.' plan_id = fields.Many2one(comodel_name="saas.plan", string="Related SaaS Plan", required=False) partner_id = fields.Many2one( comodel_name="res.partner", string="Partner", required=True, ) recurring_interval = fields.Integer( default=1, string='Billing Cycle', help="Repeat every (Days/Week/Month/Year)", ) recurring_rule_type = fields.Selection( [('daily', 'Day(s)'), ('weekly', 'Week(s)'), ('monthly', 'Month(s)'), ('monthlylastday', 'Month(s) last day'), ('yearly', 'Year(s)'), ], default='monthly', string='Recurrence', help="Specify Interval for automatic invoice generation.", readonly=True, ) # billing_criteria = fields.Selection( # selection=BILLING_CRITERIA, # string="Billing Criteria", # required=True) invoice_product_id = fields.Many2one(comodel_name="product.product", required=True, string="Invoice Product") pricelist_id = fields.Many2one( comodel_name='product.pricelist', string='Pricelist' ) currency_id = fields.Many2one(comodel_name="res.currency") contract_rate = fields.Float(string="Contract Rate") per_user_pricing = fields.Boolean(string="Per user pricing") user_cost = fields.Float(string="Per User cost") min_users = fields.Integer(string="Min. No. of users", help="""Range for Number of users in cliet's Instance""") max_users = fields.Integer(string="Max. No. of users", help="""Range for Number of users in cliet's Instance""") saas_users = fields.Integer(string="No. of users") contract_price = fields.Float(string="Contract Price", help="""Pricing for Contract""") user_billing = fields.Float(string="User Billing", help="""User Based Billing""") total_cost = fields.Float(string="Total Contract Cost") due_users_price = fields.Float(string="Due users price", default=1.0) auto_create_invoice = fields.Boolean(string="Automatically create next invoice") start_date = fields.Date( string='Purchase Date', required=True ) total_cycles = fields.Integer( string="Number of Cycles(Remaining/Total)", default=1) trial_period = fields.Integer( string="Complimentary(Free) days", default=0) @api.model def get_date_delta(self, interval): return relativedelta(months=interval) @api.onchange('user_cost', 'contract_rate', 'saas_users', 'total_cycles') def calculate_total_cost(self): for obj in self: obj.contract_price = obj.contract_rate * obj.total_cycles if obj.per_user_pricing and obj.saas_users: if obj.saas_users < obj.min_users: raise Warning("No. of users can't be less than %r"%obj.min_users) if obj.max_users != -1 and obj.saas_users > obj.max_users: raise Warning("No. of users can't be greater than %r"%obj.max_users) obj.user_billing = obj.saas_users * obj.user_cost * obj.total_cycles obj.total_cost = obj.contract_price + obj.user_billing _logger.info("+++11++++OBJ>TOTALCOST+++++++%s",obj.total_cost) @api.model def create(self, vals): if self.user_billing: vals['user_billing'] = self.saas_users * self.user_cost * self.total_cycles if self.contract_price: vals['contract_price'] = self.contract_rate * self.total_cycles if self.total_cost: vals['total_cost'] = vals['contract_price'] + vals['user_billing'] res = super(ContractCreation, self).create(vals) return res def write(self, vals): for obj in self: if not obj.user_billing: vals['user_billing'] = obj.saas_users * obj.user_cost * obj.total_cycles if not obj.contract_price: vals['contract_price'] = obj.contract_rate * obj.total_cycles if not obj.total_cost: vals['total_cost'] = vals['contract_price'] + vals['user_billing'] res = super(ContractCreation, self).write(vals) return res @api.onchange('trial_period') def trial_period_change(self): relative_delta = relativedelta(days=self.trial_period) old_date = fields.Date.from_string(fields.Date.today()) self.start_date = fields.Date.to_string(old_date + relative_delta) @api.onchange('plan_id') def plan_id_change(self): self.recurring_interval = self.plan_id.recurring_interval self.recurring_rule_type = self.plan_id.recurring_rule_type self.per_user_pricing = self.plan_id.per_user_pricing self.user_cost = self.plan_id.user_cost self.min_users = self.plan_id.min_users self.max_users = self.plan_id.max_users self.saas_users = self.plan_id.min_users self.trial_period = self.plan_id.trial_period self.contract_price = self.contract_rate * self.total_cycles self.user_billing = self.saas_users * self.user_cost * self.total_cycles self.total_cost = self.contract_price + self.user_billing self.due_users_price = self.plan_id.due_users_price relative_delta = relativedelta(days=self.trial_period) old_date = fields.Date.from_string(fields.Date.today()) self.start_date = fields.Date.to_string(old_date + relative_delta) _logger.info("=============%s",self.total_cost) _logger.info("=============%s",self.contract_price) @api.onchange('partner_id') def partner_id_change(self): self.pricelist_id = self.partner_id.property_product_pricelist and self.partner_id.property_product_pricelist.id or False self.currency_id = self.partner_id.property_product_pricelist and self.partner_id.property_product_pricelist.currency_id.id or False @api.onchange('invoice_product_id') def invoice_product_id_change(self): self.contract_rate = self.invoice_product_id and self.invoice_product_id.lst_price or False return { 'domain': {'invoice_product_id' : [('saas_plan_id', '=', self.plan_id.id)]} } def action_create_contract(self): for obj in self: if obj.per_user_pricing: obj.user_billing = obj.saas_users * obj.user_cost * obj.total_cycles if obj.saas_users < obj.min_users and obj.max_users != -1 and obj.saas_users > obj.max_users: raise Warning("Please select number of users in limit {} - {}".format(obj.min_users, obj.max_users)) obj.total_cost = obj.contract_price + obj.user_billing vals = dict( partner_id=obj.partner_id and obj.partner_id.id or False, recurring_interval=obj.recurring_interval, recurring_rule_type=obj.recurring_rule_type, invoice_product_id=obj.invoice_product_id and obj.invoice_product_id.id or False, pricelist_id=obj.partner_id.property_product_pricelist and obj.partner_id.property_product_pricelist.id or False, currency_id=obj.partner_id.property_product_pricelist and obj.partner_id.property_product_pricelist.currency_id and obj.partner_id.property_product_pricelist.currency_id.id or False, start_date=obj.start_date, total_cycles=obj.total_cycles, trial_period=obj.trial_period, remaining_cycles=obj.total_cycles, next_invoice_date=obj.start_date, contract_rate=obj.contract_rate, contract_price=obj.contract_price, due_users_price=obj.due_users_price, total_cost=obj.total_cost, per_user_pricing=obj.per_user_pricing, user_billing=obj.user_billing, user_cost=obj.user_cost, saas_users=obj.saas_users, min_users=obj.min_users, max_users=obj.max_users, auto_create_invoice=obj.auto_create_invoice, saas_module_ids=[(6, 0 , obj.plan_id.saas_module_ids.ids)], server_id=obj.plan_id.server_id.id, db_template=obj.plan_id.db_template, plan_id=obj.plan_id.id, from_backend=True, ) try: _logger.info("!!!!!!!===!!!!!!!!%s",obj.total_cost) record_id = self.env['saas.contract'].create(vals) _logger.info("--------Contract--Created-------%r", record_id) except Exception as e: _logger.info("--------Exception-While-Creating-Contract-------%r", e) else: imd = self.env['ir.model.data'] action = imd.xmlid_to_object('eagle_saas_kit.saas_contract_action') list_view_id = imd.xmlid_to_res_id('eagle_saas_kit.saas_contract_tree_view') form_view_id = imd.xmlid_to_res_id('eagle_saas_kit.saas_contract_form_view') return { 'name': action.name, 'res_id': record_id.id, 'type': action.type, 'views': [[form_view_id, 'form'], [list_view_id, 'tree'], ], 'target': action.target, 'context': action.context, 'res_model': action.res_model, }
class DeliveryCarrier(models.Model): _name = 'delivery.carrier' _description = "Shipping Methods" _order = 'sequence, id' ''' A Shipping Provider In order to add your own external provider, follow these steps: 1. Create your model MyProvider that _inherit 'delivery.carrier' 2. Extend the selection of the field "delivery_type" with a pair ('<my_provider>', 'My Provider') 3. Add your methods: <my_provider>_rate_shipment <my_provider>_send_shipping <my_provider>_get_tracking_link <my_provider>_cancel_shipment _<my_provider>_get_default_custom_package_code (they are documented hereunder) ''' # -------------------------------- # # Internals for shipping providers # # -------------------------------- # name = fields.Char('Delivery Method', required=True, translate=True) active = fields.Boolean(default=True) sequence = fields.Integer(help="Determine the display order", default=10) # This field will be overwritten by internal shipping providers by adding their own type (ex: 'fedex') delivery_type = fields.Selection([('fixed', 'Fixed Price')], string='Provider', default='fixed', required=True) integration_level = fields.Selection( [('rate', 'Get Rate'), ('rate_and_ship', 'Get Rate and Create Shipment')], string="Integration Level", default='rate_and_ship', help="Action while validating Delivery Orders") prod_environment = fields.Boolean( "Environment", help="Set to True if your credentials are certified for production.") debug_logging = fields.Boolean( 'Debug logging', help="Log requests in order to ease debugging") company_id = fields.Many2one('res.company', string='Company', related='product_id.company_id', store=True, readonly=False) product_id = fields.Many2one('product.product', string='Delivery Product', required=True, ondelete='restrict') invoice_policy = fields.Selection( [('estimated', 'Estimated cost'), ('real', 'Real cost')], string='Invoicing Policy', default='estimated', required=True, help= "Estimated Cost: the customer will be invoiced the estimated cost of the shipping.\nReal Cost: the customer will be invoiced the real cost of the shipping, the cost of the shipping will be updated on the SO after the delivery." ) country_ids = fields.Many2many('res.country', 'delivery_carrier_country_rel', 'carrier_id', 'country_id', 'Countries') state_ids = fields.Many2many('res.country.state', 'delivery_carrier_state_rel', 'carrier_id', 'state_id', 'States') zip_from = fields.Char('Zip From') zip_to = fields.Char('Zip To') margin = fields.Float( help='This percentage will be added to the shipping price.') free_over = fields.Boolean( 'Free if order amount is above', help= "If the order total amount (shipping excluded) is above or equal to this value, the customer benefits from a free shipping", default=False) amount = fields.Float( string='Amount', help= "Amount of the order to benefit from a free shipping, expressed in the company currency" ) can_generate_return = fields.Boolean( compute="_compute_can_generate_return") return_label_on_delivery = fields.Boolean( string="Generate Return Label", help="The return label is automatically generated at the delivery.") get_return_label_from_portal = fields.Boolean( string="Return Label Accessible from Customer Portal", help= "The return label can be downloaded by the customer from the customer portal." ) _sql_constraints = [ ('margin_not_under_100_percent', 'CHECK (margin >= -100)', 'Margin cannot be lower than -100%'), ] @api.depends('delivery_type') def _compute_can_generate_return(self): for carrier in self: carrier.can_generate_return = False def toggle_prod_environment(self): for c in self: c.prod_environment = not c.prod_environment def toggle_debug(self): for c in self: c.debug_logging = not c.debug_logging def install_more_provider(self): return { 'name': 'New Providers', 'view_mode': 'kanban,form', 'res_model': 'ir.module.module', 'domain': [['name', '=like', 'delivery_%'], ['name', '!=', 'delivery_barcode']], 'type': 'ir.actions.act_window', 'help': _('''<p class="o_view_nocontent"> Buy Eagle Enterprise now to get more providers. </p>'''), } def available_carriers(self, partner): return self.filtered(lambda c: c._match_address(partner)) def _match_address(self, partner): self.ensure_one() if self.country_ids and partner.country_id not in self.country_ids: return False if self.state_ids and partner.state_id not in self.state_ids: return False if self.zip_from and (partner.zip or '').upper() < self.zip_from.upper(): return False if self.zip_to and (partner.zip or '').upper() > self.zip_to.upper(): return False return True @api.onchange('integration_level') def _onchange_integration_level(self): if self.integration_level == 'rate': self.invoice_policy = 'estimated' @api.onchange('can_generate_return') def _onchange_can_generate_return(self): if not self.can_generate_return: self.return_label_on_delivery = False @api.onchange('return_label_on_delivery') def _onchange_return_label_on_delivery(self): if not self.return_label_on_delivery: self.get_return_label_from_portal = False @api.onchange('state_ids') def onchange_states(self): self.country_ids = [ (6, 0, self.country_ids.ids + self.state_ids.mapped('country_id.id')) ] @api.onchange('country_ids') def onchange_countries(self): self.state_ids = [ (6, 0, self.state_ids.filtered(lambda state: state.id in self.country_ids .mapped('state_ids').ids).ids) ] # -------------------------- # # API for external providers # # -------------------------- # def rate_shipment(self, order): ''' Compute the price of the order shipment :param order: record of sale.order :return dict: {'success': boolean, 'price': a float, 'error_message': a string containing an error message, 'warning_message': a string containing a warning message} # TODO maybe the currency code? ''' self.ensure_one() if hasattr(self, '%s_rate_shipment' % self.delivery_type): res = getattr(self, '%s_rate_shipment' % self.delivery_type)(order) # apply margin on computed price res['price'] = float(res['price']) * (1.0 + (self.margin / 100.0)) # save the real price in case a free_over rule overide it to 0 res['carrier_price'] = res['price'] # free when order is large enough if res['success'] and self.free_over and order._compute_amount_total_without_delivery( ) >= self.amount: res['warning_message'] = _( 'The shipping is free since the order amount exceeds %.2f.' ) % (self.amount) res['price'] = 0.0 return res def send_shipping(self, pickings): ''' Send the package to the service provider :param pickings: A recordset of pickings :return list: A list of dictionaries (one per picking) containing of the form:: { 'exact_price': price, 'tracking_number': number } # TODO missing labels per package # TODO missing currency # TODO missing success, error, warnings ''' self.ensure_one() if hasattr(self, '%s_send_shipping' % self.delivery_type): return getattr(self, '%s_send_shipping' % self.delivery_type)(pickings) def get_return_label(self, pickings, tracking_number=None, origin_date=None): self.ensure_one() if self.can_generate_return: return getattr(self, '%s_get_return_label' % self.delivery_type)( pickings, tracking_number, origin_date) def get_return_label_prefix(self): return 'ReturnLabel-%s' % self.delivery_type def get_tracking_link(self, picking): ''' Ask the tracking link to the service provider :param picking: record of stock.picking :return str: an URL containing the tracking link or False ''' self.ensure_one() if hasattr(self, '%s_get_tracking_link' % self.delivery_type): return getattr(self, '%s_get_tracking_link' % self.delivery_type)(picking) def cancel_shipment(self, pickings): ''' Cancel a shipment :param pickings: A recordset of pickings ''' self.ensure_one() if hasattr(self, '%s_cancel_shipment' % self.delivery_type): return getattr(self, '%s_cancel_shipment' % self.delivery_type)(pickings) def log_xml(self, xml_string, func): self.ensure_one() if self.debug_logging: self.flush() db_name = self._cr.dbname # Use a new cursor to avoid rollback that could be caused by an upper method try: db_registry = registry(db_name) with db_registry.cursor() as cr: env = api.Environment(cr, SUPERUSER_ID, {}) IrLogging = env['ir.logging'] IrLogging.sudo().create({ 'name': 'delivery.carrier', 'type': 'server', 'dbname': db_name, 'level': 'DEBUG', 'message': xml_string, 'path': self.delivery_type, 'func': func, 'line': 1 }) except psycopg2.Error: pass def _get_default_custom_package_code(self): """ Some delivery carriers require a prefix to be sent in order to use custom packages (ie not official ones). This optional method will return it as a string. """ self.ensure_one() if hasattr(self, '_%s_get_default_custom_package_code' % self.delivery_type): return getattr( self, '_%s_get_default_custom_package_code' % self.delivery_type)() else: return False # ------------------------------------------------ # # Fixed price shipping, aka a very simple provider # # ------------------------------------------------ # fixed_price = fields.Float(compute='_compute_fixed_price', inverse='_set_product_fixed_price', store=True, string='Fixed Price') @api.depends('product_id.list_price', 'product_id.product_tmpl_id.list_price') def _compute_fixed_price(self): for carrier in self: carrier.fixed_price = carrier.product_id.list_price def _set_product_fixed_price(self): for carrier in self: carrier.product_id.list_price = carrier.fixed_price def fixed_rate_shipment(self, order): carrier = self._match_address(order.partner_shipping_id) if not carrier: return { 'success': False, 'price': 0.0, 'error_message': _('Error: this delivery method is not available for this address.' ), 'warning_message': False } price = self.fixed_price if self.company_id and self.company_id.currency_id.id != order.currency_id.id: price = self.company_id.currency_id._convert( price, order.currency_id, self.company_id, fields.Date.today()) return { 'success': True, 'price': price, 'error_message': False, 'warning_message': False } def fixed_send_shipping(self, pickings): res = [] for p in pickings: res = res + [{ 'exact_price': p.carrier_id.fixed_price, 'tracking_number': False }] return res def fixed_get_tracking_link(self, picking): return False def fixed_cancel_shipment(self, pickings): raise NotImplementedError()
class AccountInvoiceLine(models.Model): _inherit = 'account.invoice.line' asset_category_id = fields.Many2one('account.asset.category', string='Asset Category') asset_start_date = fields.Date(string='Asset Start Date', compute='_get_asset_date', readonly=True, store=True) asset_end_date = fields.Date(string='Asset End Date', compute='_get_asset_date', readonly=True, store=True) asset_mrr = fields.Float(string='Monthly Recurring Revenue', compute='_get_asset_date', readonly=True, digits=dp.get_precision('Account'), store=True) @api.one @api.depends('asset_category_id', 'invoice_id.date_invoice') def _get_asset_date(self): self.asset_mrr = 0 self.asset_start_date = False self.asset_end_date = False cat = self.asset_category_id if cat: if cat.method_number == 0 or cat.method_period == 0: raise UserError(_('The number of depreciations or the period length of your asset category cannot be 0.')) months = cat.method_number * cat.method_period if self.invoice_id.type in ['out_invoice', 'out_refund']: self.asset_mrr = self.price_subtotal_signed / months if self.invoice_id.date_invoice: start_date = self.invoice_id.date_invoice.replace(day=1) end_date = (start_date + relativedelta(months=months, days=-1)) self.asset_start_date = start_date self.asset_end_date = end_date @api.one def asset_create(self): if self.asset_category_id: vals = { 'name': self.name, 'code': self.invoice_id.number or False, 'category_id': self.asset_category_id.id, 'value': self.price_subtotal_signed, 'partner_id': self.invoice_id.partner_id.id, 'company_id': self.invoice_id.company_id.id, 'currency_id': self.invoice_id.company_currency_id.id, 'date': self.invoice_id.date_invoice, 'invoice_id': self.invoice_id.id, } changed_vals = self.env['account.asset.asset'].onchange_category_id_values(vals['category_id']) vals.update(changed_vals['value']) asset = self.env['account.asset.asset'].create(vals) if self.asset_category_id.open_asset: asset.validate() return True @api.onchange('asset_category_id') def onchange_asset_category_id(self): if self.invoice_id.type == 'out_invoice' and self.asset_category_id: self.account_id = self.asset_category_id.account_asset_id.id elif self.invoice_id.type == 'in_invoice' and self.asset_category_id: self.account_id = self.asset_category_id.account_asset_id.id @api.onchange('uom_id') def _onchange_uom_id(self): result = super(AccountInvoiceLine, self)._onchange_uom_id() self.onchange_asset_category_id() return result @api.onchange('product_id') def _onchange_product_id(self): vals = super(AccountInvoiceLine, self)._onchange_product_id() if self.product_id: if self.invoice_id.type == 'out_invoice': self.asset_category_id = self.product_id.product_tmpl_id.deferred_revenue_category_id elif self.invoice_id.type == 'in_invoice': self.asset_category_id = self.product_id.product_tmpl_id.asset_category_id return vals def _set_additional_fields(self, invoice): if not self.asset_category_id: if invoice.type == 'out_invoice': self.asset_category_id = self.product_id.product_tmpl_id.deferred_revenue_category_id.id elif invoice.type == 'in_invoice': self.asset_category_id = self.product_id.product_tmpl_id.asset_category_id.id self.onchange_asset_category_id() super(AccountInvoiceLine, self)._set_additional_fields(invoice) def get_invoice_line_account(self, type, product, fpos, company): return product.asset_category_id.account_asset_id or super(AccountInvoiceLine, self).get_invoice_line_account(type, product, fpos, company)
class FleetVehicleLogContract(models.Model): _inherit = 'fleet.vehicle.log.contract' recurring_cost_amount_depreciated = fields.Float( "Recurring Cost Amount (depreciated)", track_visibility="onchange")
class SurveyUserInputLine(models.Model): _name = 'survey.user_input_line' _description = 'Survey User Input Line' _rec_name = 'user_input_id' _order = 'question_sequence,id' # survey data user_input_id = fields.Many2one('survey.user_input', string='User Input', ondelete='cascade', required=True) survey_id = fields.Many2one(related='user_input_id.survey_id', string='Survey', store=True, readonly=False) question_id = fields.Many2one('survey.question', string='Question', ondelete='cascade', required=True) page_id = fields.Many2one(related='question_id.page_id', string="Section", readonly=False) question_sequence = fields.Integer('Sequence', related='question_id.sequence', store=True) # answer skipped = fields.Boolean('Skipped') answer_type = fields.Selection([('text', 'Text'), ('number', 'Number'), ('date', 'Date'), ('datetime', 'Datetime'), ('free_text', 'Free Text'), ('suggestion', 'Suggestion')], string='Answer Type') value_text = fields.Char('Text answer') value_number = fields.Float('Numerical answer') value_date = fields.Date('Date answer') value_datetime = fields.Datetime('Datetime answer') value_free_text = fields.Text('Free Text answer') value_suggested = fields.Many2one('survey.label', string="Suggested answer") value_suggested_row = fields.Many2one('survey.label', string="Row answer") answer_score = fields.Float('Score') answer_is_correct = fields.Boolean('Correct', compute='_compute_answer_is_correct') @api.depends('value_suggested', 'question_id') def _compute_answer_is_correct(self): for answer in self: if answer.value_suggested and answer.question_id.question_type in [ 'simple_choice', 'multiple_choice' ]: answer.answer_is_correct = answer.value_suggested.is_correct else: answer.answer_is_correct = False @api.constrains('skipped', 'answer_type') def _answered_or_skipped(self): for uil in self: if not uil.skipped != bool(uil.answer_type): raise ValidationError( _('This question cannot be unanswered or skipped.')) @api.constrains('answer_type') def _check_answer_type(self): for uil in self: fields_type = { 'text': bool(uil.value_text), 'number': (bool(uil.value_number) or uil.value_number == 0), 'date': bool(uil.value_date), 'free_text': bool(uil.value_free_text), 'suggestion': bool(uil.value_suggested) } if not fields_type.get(uil.answer_type, True): raise ValidationError( _('The answer must be in the right type')) @api.model_create_multi def create(self, vals_list): for vals in vals_list: value_suggested = vals.get('value_suggested') if value_suggested: vals.update({ 'answer_score': self.env['survey.label'].browse( int(value_suggested)).answer_score }) return super(SurveyUserInputLine, self).create(vals_list) def write(self, vals): value_suggested = vals.get('value_suggested') if value_suggested: vals.update({ 'answer_score': self.env['survey.label'].browse( int(value_suggested)).answer_score }) return super(SurveyUserInputLine, self).write(vals) @api.model def save_lines(self, user_input_id, question, post, answer_tag): """ Save answers to questions, depending on question type If an answer already exists for question and user_input_id, it will be overwritten (in order to maintain data consistency). """ try: saver = getattr(self, 'save_line_' + question.question_type) except AttributeError: _logger.error(question.question_type + ": This type of question has no saving function") return False else: saver(user_input_id, question, post, answer_tag) @api.model def save_line_free_text(self, user_input_id, question, post, answer_tag): vals = { 'user_input_id': user_input_id, 'question_id': question.id, 'survey_id': question.survey_id.id, 'skipped': False, } if answer_tag in post and post[answer_tag].strip(): vals.update({ 'answer_type': 'free_text', 'value_free_text': post[answer_tag] }) else: vals.update({'answer_type': None, 'skipped': True}) old_uil = self.search([('user_input_id', '=', user_input_id), ('survey_id', '=', question.survey_id.id), ('question_id', '=', question.id)]) if old_uil: old_uil.write(vals) else: old_uil.create(vals) return True @api.model def save_line_textbox(self, user_input_id, question, post, answer_tag): vals = { 'user_input_id': user_input_id, 'question_id': question.id, 'survey_id': question.survey_id.id, 'skipped': False } if answer_tag in post and post[answer_tag].strip(): vals.update({ 'answer_type': 'text', 'value_text': post[answer_tag] }) else: vals.update({'answer_type': None, 'skipped': True}) old_uil = self.search([('user_input_id', '=', user_input_id), ('survey_id', '=', question.survey_id.id), ('question_id', '=', question.id)]) if old_uil: old_uil.write(vals) else: old_uil.create(vals) return True @api.model def save_line_numerical_box(self, user_input_id, question, post, answer_tag): vals = { 'user_input_id': user_input_id, 'question_id': question.id, 'survey_id': question.survey_id.id, 'skipped': False } if answer_tag in post and post[answer_tag].strip(): vals.update({ 'answer_type': 'number', 'value_number': float(post[answer_tag]) }) else: vals.update({'answer_type': None, 'skipped': True}) old_uil = self.search([('user_input_id', '=', user_input_id), ('survey_id', '=', question.survey_id.id), ('question_id', '=', question.id)]) if old_uil: old_uil.write(vals) else: old_uil.create(vals) return True @api.model def save_line_date(self, user_input_id, question, post, answer_tag): vals = { 'user_input_id': user_input_id, 'question_id': question.id, 'survey_id': question.survey_id.id, 'skipped': False } if answer_tag in post and post[answer_tag].strip(): vals.update({ 'answer_type': 'date', 'value_date': post[answer_tag] }) else: vals.update({'answer_type': None, 'skipped': True}) old_uil = self.search([('user_input_id', '=', user_input_id), ('survey_id', '=', question.survey_id.id), ('question_id', '=', question.id)]) if old_uil: old_uil.write(vals) else: old_uil.create(vals) return True @api.model def save_line_datetime(self, user_input_id, question, post, answer_tag): vals = { 'user_input_id': user_input_id, 'question_id': question.id, 'survey_id': question.survey_id.id, 'skipped': False } if answer_tag in post and post[answer_tag].strip(): vals.update({ 'answer_type': 'datetime', 'value_datetime': post[answer_tag] }) else: vals.update({'answer_type': None, 'skipped': True}) old_uil = self.search([('user_input_id', '=', user_input_id), ('survey_id', '=', question.survey_id.id), ('question_id', '=', question.id)]) if old_uil: old_uil.write(vals) else: old_uil.create(vals) return True @api.model def save_line_simple_choice(self, user_input_id, question, post, answer_tag): vals = { 'user_input_id': user_input_id, 'question_id': question.id, 'survey_id': question.survey_id.id, 'skipped': False } old_uil = self.search([('user_input_id', '=', user_input_id), ('survey_id', '=', question.survey_id.id), ('question_id', '=', question.id)]) old_uil.sudo().unlink() if answer_tag in post and post[answer_tag].strip(): vals.update({ 'answer_type': 'suggestion', 'value_suggested': int(post[answer_tag]) }) else: vals.update({'answer_type': None, 'skipped': True}) # '-1' indicates 'comment count as an answer' so do not need to record it if post.get(answer_tag) and post.get(answer_tag) != '-1': self.create(vals) comment_answer = post.pop(("%s_%s" % (answer_tag, 'comment')), '').strip() if comment_answer: vals.update({ 'answer_type': 'text', 'value_text': comment_answer, 'skipped': False, 'value_suggested': False }) self.create(vals) return True @api.model def save_line_multiple_choice(self, user_input_id, question, post, answer_tag): vals = { 'user_input_id': user_input_id, 'question_id': question.id, 'survey_id': question.survey_id.id, 'skipped': False } old_uil = self.search([('user_input_id', '=', user_input_id), ('survey_id', '=', question.survey_id.id), ('question_id', '=', question.id)]) old_uil.sudo().unlink() ca_dict = dict_keys_startswith(post, answer_tag + '_') comment_answer = ca_dict.pop(("%s_%s" % (answer_tag, 'comment')), '').strip() if len(ca_dict) > 0: for key in ca_dict: # '-1' indicates 'comment count as an answer' so do not need to record it if key != ('%s_%s' % (answer_tag, '-1')): val = ca_dict[key] vals.update({ 'answer_type': 'suggestion', 'value_suggested': bool(val) and int(val) }) self.create(vals) if comment_answer: vals.update({ 'answer_type': 'text', 'value_text': comment_answer, 'value_suggested': False }) self.create(vals) if not ca_dict and not comment_answer: vals.update({'answer_type': None, 'skipped': True}) self.create(vals) return True @api.model def save_line_matrix(self, user_input_id, question, post, answer_tag): vals = { 'user_input_id': user_input_id, 'question_id': question.id, 'survey_id': question.survey_id.id, 'skipped': False } old_uil = self.search([('user_input_id', '=', user_input_id), ('survey_id', '=', question.survey_id.id), ('question_id', '=', question.id)]) old_uil.sudo().unlink() no_answers = True ca_dict = dict_keys_startswith(post, answer_tag + '_') comment_answer = ca_dict.pop(("%s_%s" % (answer_tag, 'comment')), '').strip() if comment_answer: vals.update({'answer_type': 'text', 'value_text': comment_answer}) self.create(vals) no_answers = False if question.matrix_subtype == 'simple': for row in question.labels_ids_2: a_tag = "%s_%s" % (answer_tag, row.id) if a_tag in ca_dict: no_answers = False vals.update({ 'answer_type': 'suggestion', 'value_suggested': ca_dict[a_tag], 'value_suggested_row': row.id }) self.create(vals) elif question.matrix_subtype == 'multiple': for col in question.labels_ids: for row in question.labels_ids_2: a_tag = "%s_%s_%s" % (answer_tag, row.id, col.id) if a_tag in ca_dict: no_answers = False vals.update({ 'answer_type': 'suggestion', 'value_suggested': col.id, 'value_suggested_row': row.id }) self.create(vals) if no_answers: vals.update({'answer_type': None, 'skipped': True}) self.create(vals) return True
class SaleAdvancePaymentInv(models.TransientModel): _name = "sale.advance.payment.inv" _description = "Sales Advance Payment Invoice" @api.model def _count(self): return len(self._context.get('active_ids', [])) @api.model def _default_product_id(self): product_id = self.env['ir.config_parameter'].sudo().get_param('sale.default_deposit_product_id') return self.env['product.product'].browse(int(product_id)).exists() @api.model def _default_deposit_account_id(self): return self._default_product_id().property_account_income_id @api.model def _default_deposit_taxes_id(self): return self._default_product_id().taxes_id @api.model def _default_has_down_payment(self): if self._context.get('active_model') == 'sale.order' and self._context.get('active_id', False): sale_order = self.env['sale.order'].browse(self._context.get('active_id')) return sale_order.order_line.filtered( lambda sale_order_line: sale_order_line.is_downpayment ) return False @api.model def _default_currency_id(self): if self._context.get('active_model') == 'sale.order' and self._context.get('active_id', False): sale_order = self.env['sale.order'].browse(self._context.get('active_id')) return sale_order.currency_id advance_payment_method = fields.Selection([ ('delivered', 'Regular invoice'), ('percentage', 'Down payment (percentage)'), ('fixed', 'Down payment (fixed amount)') ], string='Create Invoice', default='delivered', required=True, help="A standard invoice is issued with all the order lines ready for invoicing, \ according to their invoicing policy (based on ordered or delivered quantity).") deduct_down_payments = fields.Boolean('Deduct down payments', default=True) has_down_payments = fields.Boolean('Has down payments', default=_default_has_down_payment, readonly=True) product_id = fields.Many2one('product.product', string='Down Payment Product', domain=[('type', '=', 'service')], default=_default_product_id) count = fields.Integer(default=_count, string='Order Count') amount = fields.Float('Down Payment Amount', digits='Account', help="The percentage of amount to be invoiced in advance, taxes excluded.") currency_id = fields.Many2one('res.currency', string='Currency', default=_default_currency_id) fixed_amount = fields.Monetary('Down Payment Amount(Fixed)', help="The fixed amount to be invoiced in advance, taxes excluded.") deposit_account_id = fields.Many2one("account.account", string="Income Account", domain=[('deprecated', '=', False)], help="Account used for deposits", default=_default_deposit_account_id) deposit_taxes_id = fields.Many2many("account.tax", string="Customer Taxes", help="Taxes used for deposits", default=_default_deposit_taxes_id) @api.onchange('advance_payment_method') def onchange_advance_payment_method(self): if self.advance_payment_method == 'percentage': return {'value': {'amount': 0}} return {} def _create_invoice(self, order, so_line, amount): if (self.advance_payment_method == 'percentage' and self.amount <= 0.00) or (self.advance_payment_method == 'fixed' and self.fixed_amount <= 0.00): raise UserError(_('The value of the down payment amount must be positive.')) if self.advance_payment_method == 'percentage': amount = order.amount_untaxed * self.amount / 100 name = _("Down payment of %s%%") % (self.amount,) else: amount = self.fixed_amount name = _('Down Payment') invoice_vals = { 'type': 'out_invoice', 'invoice_origin': order.name, 'invoice_user_id': order.user_id.id, 'narration': order.note, 'partner_id': order.partner_invoice_id.id, 'fiscal_position_id': order.fiscal_position_id.id or order.partner_id.property_account_position_id.id, 'partner_shipping_id': order.partner_shipping_id.id, 'currency_id': order.pricelist_id.currency_id.id, 'invoice_payment_ref': order.client_order_ref, 'invoice_payment_term_id': order.payment_term_id.id, 'invoice_partner_bank_id': order.company_id.partner_id.bank_ids[:1], 'team_id': order.team_id.id, 'campaign_id': order.campaign_id.id, 'medium_id': order.medium_id.id, 'source_id': order.source_id.id, 'invoice_line_ids': [(0, 0, { 'name': name, 'price_unit': amount, 'quantity': 1.0, 'product_id': self.product_id.id, 'product_uom_id': so_line.product_uom.id, 'tax_ids': [(6, 0, so_line.tax_id.ids)], 'sale_line_ids': [(6, 0, [so_line.id])], 'analytic_tag_ids': [(6, 0, so_line.analytic_tag_ids.ids)], 'analytic_account_id': order.analytic_account_id.id or False, })], } if order.fiscal_position_id: invoice_vals['fiscal_position_id'] = order.fiscal_position_id.id invoice = self.env['account.move'].create(invoice_vals) invoice.message_post_with_view('mail.message_origin_link', values={'self': invoice, 'origin': order}, subtype_id=self.env.ref('mail.mt_note').id) return invoice def create_invoices(self): sale_orders = self.env['sale.order'].browse(self._context.get('active_ids', [])) if self.advance_payment_method == 'delivered': sale_orders._create_invoices(final=self.deduct_down_payments) else: # Create deposit product if necessary if not self.product_id: vals = self._prepare_deposit_product() self.product_id = self.env['product.product'].create(vals) self.env['ir.config_parameter'].sudo().set_param('sale.default_deposit_product_id', self.product_id.id) sale_line_obj = self.env['sale.order.line'] for order in sale_orders: if self.advance_payment_method == 'percentage': amount = order.amount_untaxed * self.amount / 100 else: amount = self.fixed_amount if self.product_id.invoice_policy != 'order': raise UserError(_('The product used to invoice a down payment should have an invoice policy set to "Ordered quantities". Please update your deposit product to be able to create a deposit invoice.')) if self.product_id.type != 'service': raise UserError(_("The product used to invoice a down payment should be of type 'Service'. Please use another product or update this product.")) taxes = self.product_id.taxes_id.filtered(lambda r: not order.company_id or r.company_id == order.company_id) if order.fiscal_position_id and taxes: tax_ids = order.fiscal_position_id.map_tax(taxes, self.product_id, order.partner_shipping_id).ids else: tax_ids = taxes.ids context = {'lang': order.partner_id.lang} analytic_tag_ids = [] for line in order.order_line: analytic_tag_ids = [(4, analytic_tag.id, None) for analytic_tag in line.analytic_tag_ids] so_line = sale_line_obj.create({ 'name': _('Down Payment: %s') % (time.strftime('%m %Y'),), 'price_unit': amount, 'product_uom_qty': 0.0, 'order_id': order.id, 'discount': 0.0, 'product_uom': self.product_id.uom_id.id, 'product_id': self.product_id.id, 'analytic_tag_ids': analytic_tag_ids, 'tax_id': [(6, 0, tax_ids)], 'is_downpayment': True, }) del context self._create_invoice(order, so_line, amount) if self._context.get('open_invoices', False): return sale_orders.action_view_invoice() return {'type': 'ir.actions.act_window_close'} def _prepare_deposit_product(self): return { 'name': 'Down payment', 'type': 'service', 'invoice_policy': 'order', 'property_account_income_id': self.deposit_account_id.id, 'taxes_id': [(6, 0, self.deposit_taxes_id.ids)], 'company_id': False, }
class SurveyUserInput(models.Model): """ Metadata for a set of one user's answers to a particular survey """ _name = "survey.user_input" _rec_name = 'survey_id' _description = 'Survey User Input' # description survey_id = fields.Many2one('survey.survey', string='Survey', required=True, readonly=True, ondelete='cascade') scoring_type = fields.Selection(string="Scoring", related="survey_id.scoring_type") is_attempts_limited = fields.Boolean( "Limited number of attempts", related='survey_id.is_attempts_limited') attempts_limit = fields.Integer("Number of attempts", related='survey_id.attempts_limit') start_datetime = fields.Datetime('Start date and time', readonly=True) is_time_limit_reached = fields.Boolean( "Is time limit reached?", compute='_compute_is_time_limit_reached') input_type = fields.Selection([('manually', 'Manual'), ('link', 'Invitation')], string='Answer Type', default='manually', required=True, readonly=True) state = fields.Selection([('new', 'Not started yet'), ('skip', 'Partially completed'), ('done', 'Completed')], string='Status', default='new', readonly=True) test_entry = fields.Boolean(readonly=True) # identification and access token = fields.Char('Identification token', default=lambda self: str(uuid.uuid4()), readonly=True, required=True, copy=False) # no unique constraint, as it identifies a pool of attempts invite_token = fields.Char('Invite token', readonly=True, copy=False) partner_id = fields.Many2one('res.partner', string='Partner', readonly=True) email = fields.Char('E-mail', readonly=True) attempt_number = fields.Integer("Attempt n°", compute='_compute_attempt_number') # Displaying data last_displayed_page_id = fields.Many2one( 'survey.question', string='Last displayed question/page') # answers user_input_line_ids = fields.One2many('survey.user_input_line', 'user_input_id', string='Answers', copy=True) # Pre-defined questions question_ids = fields.Many2many('survey.question', string='Predefined Questions', readonly=True) deadline = fields.Datetime( 'Deadline', help="Datetime until customer can open the survey and submit answers") # Stored for performance reasons while displaying results page quizz_score = fields.Float("Score (%)", compute="_compute_quizz_score", store=True, compute_sudo=True) quizz_passed = fields.Boolean('Quizz Passed', compute='_compute_quizz_passed', store=True, compute_sudo=True) @api.depends('user_input_line_ids.answer_score', 'user_input_line_ids.question_id') def _compute_quizz_score(self): for user_input in self: total_possible_score = sum([ answer_score if answer_score > 0 else 0 for answer_score in user_input.question_ids.mapped('labels_ids.answer_score') ]) if total_possible_score == 0: user_input.quizz_score = 0 else: score = (sum( user_input.user_input_line_ids.mapped('answer_score')) / total_possible_score) * 100 user_input.quizz_score = round(score, 2) if score > 0 else 0 @api.depends('quizz_score', 'survey_id.passing_score') def _compute_quizz_passed(self): for user_input in self: user_input.quizz_passed = user_input.quizz_score >= user_input.survey_id.passing_score _sql_constraints = [ ('unique_token', 'UNIQUE (token)', 'A token must be unique!'), ] @api.model def do_clean_emptys(self): """ Remove empty user inputs that have been created manually (used as a cronjob declared in data/survey_cron.xml) """ an_hour_ago = fields.Datetime.to_string(datetime.datetime.now() - datetime.timedelta(hours=1)) self.search([('input_type', '=', 'manually'), ('state', '=', 'new'), ('create_date', '<', an_hour_ago)]).unlink() @api.model def _generate_invite_token(self): return str(uuid.uuid4()) def action_resend(self): partners = self.env['res.partner'] emails = [] for user_answer in self: if user_answer.partner_id: partners |= user_answer.partner_id elif user_answer.email: emails.append(user_answer.email) return self.survey_id.with_context( default_existing_mode='resend', default_partner_ids=partners.ids, default_emails=','.join(emails)).action_send_survey() def action_print_answers(self): """ Open the website page with the survey form """ self.ensure_one() return { 'type': 'ir.actions.act_url', 'name': "View Answers", 'target': 'self', 'url': '/survey/print/%s?answer_token=%s' % (self.survey_id.access_token, self.token) } @api.depends('start_datetime', 'survey_id.is_time_limited', 'survey_id.time_limit') def _compute_is_time_limit_reached(self): """ Checks that the user_input is not exceeding the survey's time limit. """ for user_input in self: user_input.is_time_limit_reached = user_input.survey_id.is_time_limited and fields.Datetime.now() \ > user_input.start_datetime + relativedelta(minutes=user_input.survey_id.time_limit) @api.depends('state', 'test_entry', 'survey_id.is_attempts_limited', 'partner_id', 'email', 'invite_token') def _compute_attempt_number(self): attempts_to_compute = self.filtered( lambda user_input: user_input.state == 'done' and not user_input. test_entry and user_input.survey_id.is_attempts_limited) for user_input in (self - attempts_to_compute): user_input.attempt_number = 1 if attempts_to_compute: self.env.cr.execute( """SELECT user_input.id, (COUNT(previous_user_input.id) + 1) AS attempt_number FROM survey_user_input user_input LEFT OUTER JOIN survey_user_input previous_user_input ON user_input.survey_id = previous_user_input.survey_id AND previous_user_input.state = 'done' AND previous_user_input.test_entry = False AND previous_user_input.id < user_input.id AND (user_input.invite_token IS NULL OR user_input.invite_token = previous_user_input.invite_token) AND (user_input.partner_id = previous_user_input.partner_id OR user_input.email = previous_user_input.email) WHERE user_input.id IN %s GROUP BY user_input.id; """, (tuple(attempts_to_compute.ids), )) attempts_count_results = self.env.cr.dictfetchall() for user_input in attempts_to_compute: attempt_number = 1 for attempts_count_result in attempts_count_results: if attempts_count_result['id'] == user_input.id: attempt_number = attempts_count_result[ 'attempt_number'] break user_input.attempt_number = attempt_number def _mark_done(self): """ This method will: 1. mark the state as 'done' 2. send the certification email with attached document if - The survey is a certification - It has a certification_mail_template_id set - The user succeeded the test Will also run challenge Cron to give the certification badge if any.""" self.write({'state': 'done'}) Challenge = self.env['gamification.challenge'].sudo() badge_ids = [] for user_input in self: if user_input.survey_id.certificate and user_input.quizz_passed: if user_input.survey_id.certification_mail_template_id and not user_input.test_entry: user_input.survey_id.certification_mail_template_id.send_mail( user_input.id, notif_layout="mail.mail_notification_light") if user_input.survey_id.certification_give_badge: badge_ids.append( user_input.survey_id.certification_badge_id.id) if badge_ids: challenges = Challenge.search([('reward_id', 'in', badge_ids)]) if challenges: Challenge._cron_update(ids=challenges.ids, commit=False) def _get_survey_url(self): self.ensure_one() return '/survey/start/%s?answer_token=%s' % ( self.survey_id.access_token, self.token)
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 ProductTemplateAttributeValue(models.Model): _inherit = "product.template.attribute.value" price_factor = fields.Float( "Price Factor", digits=dp.get_precision("Product Price"), default=1.0 )
class HrEmployeeBase(models.AbstractModel): _inherit = "hr.employee.base" leave_manager_id = fields.Many2one( 'res.users', string='Time Off', help="User responsible of leaves approval.") remaining_leaves = fields.Float( compute='_compute_remaining_leaves', string='Remaining Paid Time Off', help= 'Total number of paid time off allocated to this employee, change this value to create allocation/time off request. ' 'Total based on all the time off types without overriding limit.') current_leave_state = fields.Selection(compute='_compute_leave_status', string="Current Time Off Status", selection=[ ('draft', 'New'), ('confirm', 'Waiting Approval'), ('refuse', 'Refused'), ('validate1', 'Waiting Second Approval'), ('validate', 'Approved'), ('cancel', 'Cancelled') ]) current_leave_id = fields.Many2one('hr.leave.type', compute='_compute_leave_status', string="Current Time Off Type") leave_date_from = fields.Date('From Date', compute='_compute_leave_status') leave_date_to = fields.Date('To Date', compute='_compute_leave_status') leaves_count = fields.Float('Number of Time Off', compute='_compute_remaining_leaves') allocation_count = fields.Float('Total number of days allocated.', compute='_compute_allocation_count') allocation_used_count = fields.Float( 'Total number of days off used', compute='_compute_total_allocation_used') show_leaves = fields.Boolean('Able to see Remaining Time Off', compute='_compute_show_leaves') is_absent = fields.Boolean('Absent Today', compute='_compute_leave_status', search='_search_absent_employee') allocation_display = fields.Char(compute='_compute_allocation_count') allocation_used_display = fields.Char( compute='_compute_total_allocation_used') def _get_date_start_work(self): return self.create_date def _get_remaining_leaves(self): """ Helper to compute the remaining leaves for the current employees :returns dict where the key is the employee id, and the value is the remain leaves """ self._cr.execute( """ SELECT sum(h.number_of_days) AS days, h.employee_id FROM ( SELECT holiday_status_id, number_of_days, state, employee_id FROM hr_leave_allocation UNION ALL SELECT holiday_status_id, (number_of_days * -1) as number_of_days, state, employee_id FROM hr_leave ) h join hr_leave_type s ON (s.id=h.holiday_status_id) WHERE s.active = true AND h.state='validate' AND (s.allocation_type='fixed' OR s.allocation_type='fixed_allocation') AND h.employee_id in %s GROUP BY h.employee_id""", (tuple(self.ids), )) return dict((row['employee_id'], row['days']) for row in self._cr.dictfetchall()) def _compute_remaining_leaves(self): remaining = self._get_remaining_leaves() for employee in self: value = float_round(remaining.get(employee.id, 0.0), precision_digits=2) employee.leaves_count = value employee.remaining_leaves = value def _compute_allocation_count(self): for employee in self: allocations = self.env['hr.leave.allocation'].search([ ('employee_id', '=', employee.id), ('holiday_status_id.active', '=', True), ('state', '=', 'validate'), '|', ('date_to', '=', False), ('date_to', '>=', datetime.date.today()), ]) employee.allocation_count = sum( allocations.mapped('number_of_days')) employee.allocation_display = "%g" % employee.allocation_count def _compute_total_allocation_used(self): for employee in self: employee.allocation_used_count = employee.allocation_count - employee.remaining_leaves employee.allocation_used_display = "%g" % employee.allocation_used_count def _compute_presence_state(self): super()._compute_presence_state() employees = self.filtered(lambda employee: employee.hr_presence_state != 'present' and employee.is_absent) employees.update({'hr_presence_state': 'absent'}) def _compute_leave_status(self): # Used SUPERUSER_ID to forcefully get status of other user's leave, to bypass record rule holidays = self.env['hr.leave'].sudo().search([ ('employee_id', 'in', self.ids), ('date_from', '<=', fields.Datetime.now()), ('date_to', '>=', fields.Datetime.now()), ('state', 'not in', ('cancel', 'refuse')) ]) leave_data = {} for holiday in holidays: leave_data[holiday.employee_id.id] = {} leave_data[holiday.employee_id. id]['leave_date_from'] = holiday.date_from.date() leave_data[holiday.employee_id. id]['leave_date_to'] = holiday.date_to.date() leave_data[ holiday.employee_id.id]['current_leave_state'] = holiday.state leave_data[holiday.employee_id. id]['current_leave_id'] = holiday.holiday_status_id.id for employee in self: employee.leave_date_from = leave_data.get( employee.id, {}).get('leave_date_from') employee.leave_date_to = leave_data.get(employee.id, {}).get('leave_date_to') employee.current_leave_state = leave_data.get( employee.id, {}).get('current_leave_state') employee.current_leave_id = leave_data.get( employee.id, {}).get('current_leave_id') employee.is_absent = leave_data.get( employee.id) and leave_data.get( employee.id, {}).get('current_leave_state') not in [ 'cancel', 'refuse', 'draft' ] @api.onchange('parent_id') def _onchange_parent_id(self): super(HrEmployeeBase, self)._onchange_parent_id() previous_manager = self._origin.parent_id.user_id manager = self.parent_id.user_id if manager and self.leave_manager_id == previous_manager or not self.leave_manager_id: self.leave_manager_id = manager def _compute_show_leaves(self): show_leaves = self.env['res.users'].has_group( 'hr_holidays.group_hr_holidays_user') for employee in self: if show_leaves or employee.user_id == self.env.user: employee.show_leaves = True else: employee.show_leaves = False def _search_absent_employee(self, operator, value): holidays = self.env['hr.leave'].sudo().search([ ('employee_id', '!=', False), ('state', 'not in', ['cancel', 'refuse']), ('date_from', '<=', datetime.datetime.utcnow()), ('date_to', '>=', datetime.datetime.utcnow()) ]) return [('id', 'in', holidays.mapped('employee_id').ids)] @api.model def create(self, values): if 'parent_id' in values: manager = self.env['hr.employee'].browse( values['parent_id']).user_id values['leave_manager_id'] = values.get('leave_manager_id', manager.id) return super(HrEmployeeBase, self).create(values) def write(self, values): if 'parent_id' in values: manager = self.env['hr.employee'].browse( values['parent_id']).user_id if manager: to_change = self.filtered( lambda e: e.leave_manager_id == e.parent_id.user_id or not e.leave_manager_id) to_change.write({ 'leave_manager_id': values.get('leave_manager_id', manager.id) }) res = super(HrEmployeeBase, self).write(values) if 'parent_id' in values or 'department_id' in values: today_date = fields.Datetime.now() hr_vals = {} if values.get('parent_id') is not None: hr_vals['manager_id'] = values['parent_id'] if values.get('department_id') is not None: hr_vals['department_id'] = values['department_id'] holidays = self.env['hr.leave'].sudo().search([ '|', ('state', 'in', ['draft', 'confirm']), ('date_from', '>', today_date), ('employee_id', 'in', self.ids) ]) holidays.write(hr_vals) allocations = self.env['hr.leave.allocation'].sudo().search([ ('state', 'in', ['draft', 'confirm']), ('employee_id', 'in', self.ids) ]) allocations.write(hr_vals) return res
class Task(models.Model): _name = "project.task" _description = "Task" _date_name = "date_assign" _inherit = ['portal.mixin', 'mail.thread.cc', 'mail.activity.mixin', 'rating.mixin'] _mail_post_access = 'read' _order = "priority desc, sequence, id desc" _check_company_auto = True @api.model def default_get(self, fields_list): result = super(Task, self).default_get(fields_list) # find default value from parent for the not given ones parent_task_id = result.get('parent_id') or self._context.get('default_parent_id') if parent_task_id: parent_values = self._subtask_values_from_parent(parent_task_id) for fname, value in parent_values.items(): if fname not in result: result[fname] = value return result @api.model def _get_default_partner(self): if 'default_project_id' in self.env.context: default_project_id = self.env['project.project'].browse(self.env.context['default_project_id']) return default_project_id.exists().partner_id def _get_default_stage_id(self): """ Gives default stage_id """ project_id = self.env.context.get('default_project_id') if not project_id: return False return self.stage_find(project_id, [('fold', '=', False)]) @api.model def _default_company_id(self): if self._context.get('default_project_id'): return self.env['project.project'].browse(self._context['default_project_id']).company_id return self.env.company @api.model def _read_group_stage_ids(self, stages, domain, order): search_domain = [('id', 'in', stages.ids)] if 'default_project_id' in self.env.context: search_domain = ['|', ('project_ids', '=', self.env.context['default_project_id'])] + search_domain stage_ids = stages._search(search_domain, order=order, access_rights_uid=SUPERUSER_ID) return stages.browse(stage_ids) active = fields.Boolean(default=True) name = fields.Char(string='Title', tracking=True, required=True, index=True) description = fields.Html(string='Description') priority = fields.Selection([ ('0', 'Normal'), ('1', 'Important'), ], default='0', index=True, string="Priority") sequence = fields.Integer(string='Sequence', index=True, default=10, help="Gives the sequence order when displaying a list of tasks.") stage_id = fields.Many2one('project.task.type', string='Stage', ondelete='restrict', tracking=True, index=True, default=_get_default_stage_id, group_expand='_read_group_stage_ids', domain="[('project_ids', '=', project_id)]", copy=False) tag_ids = fields.Many2many('project.tags', string='Tags') kanban_state = fields.Selection([ ('normal', 'Grey'), ('done', 'Green'), ('blocked', 'Red')], string='Kanban State', copy=False, default='normal', required=True) kanban_state_label = fields.Char(compute='_compute_kanban_state_label', string='Kanban State Label', tracking=True) create_date = fields.Datetime("Created On", readonly=True, index=True) write_date = fields.Datetime("Last Updated On", readonly=True, index=True) date_end = fields.Datetime(string='Ending Date', index=True, copy=False) date_assign = fields.Datetime(string='Assigning Date', index=True, copy=False, readonly=True) date_deadline = fields.Date(string='Deadline', index=True, copy=False, tracking=True) date_deadline_formatted = fields.Char(compute='_compute_date_deadline_formatted') date_last_stage_update = fields.Datetime(string='Last Stage Update', index=True, copy=False, readonly=True) project_id = fields.Many2one('project.project', string='Project', default=lambda self: self.env.context.get('default_project_id'), index=True, tracking=True, check_company=True, change_default=True) planned_hours = fields.Float("Planned Hours", help='It is the time planned to achieve the task. If this document has sub-tasks, it means the time needed to achieve this tasks and its childs.',tracking=True) subtask_planned_hours = fields.Float("Subtasks", compute='_compute_subtask_planned_hours', help="Computed using sum of hours planned of all subtasks created from main task. Usually these hours are less or equal to the Planned Hours (of main task).") user_id = fields.Many2one('res.users', string='Assigned to', default=lambda self: self.env.uid, index=True, tracking=True) partner_id = fields.Many2one('res.partner', string='Customer', default=lambda self: self._get_default_partner(), domain="['|', ('company_id', '=', False), ('company_id', '=', company_id)]") partner_city = fields.Char(related='partner_id.city', readonly=False) manager_id = fields.Many2one('res.users', string='Project Manager', related='project_id.user_id', readonly=True, related_sudo=False) company_id = fields.Many2one('res.company', string='Company', required=True, default=_default_company_id) color = fields.Integer(string='Color Index') user_email = fields.Char(related='user_id.email', string='User Email', readonly=True, related_sudo=False) attachment_ids = fields.One2many('ir.attachment', compute='_compute_attachment_ids', string="Main Attachments", help="Attachment that don't come from message.") # In the domain of displayed_image_id, we couln't use attachment_ids because a one2many is represented as a list of commands so we used res_model & res_id displayed_image_id = fields.Many2one('ir.attachment', domain="[('res_model', '=', 'project.task'), ('res_id', '=', id), ('mimetype', 'ilike', 'image')]", string='Cover Image') legend_blocked = fields.Char(related='stage_id.legend_blocked', string='Kanban Blocked Explanation', readonly=True, related_sudo=False) legend_done = fields.Char(related='stage_id.legend_done', string='Kanban Valid Explanation', readonly=True, related_sudo=False) legend_normal = fields.Char(related='stage_id.legend_normal', string='Kanban Ongoing Explanation', readonly=True, related_sudo=False) parent_id = fields.Many2one('project.task', string='Parent Task', index=True) child_ids = fields.One2many('project.task', 'parent_id', string="Sub-tasks", context={'active_test': False}) subtask_project_id = fields.Many2one('project.project', related="project_id.subtask_project_id", string='Sub-task Project', readonly=True) subtask_count = fields.Integer("Sub-task count", compute='_compute_subtask_count') email_from = fields.Char(string='Email', help="These people will receive email.", index=True) # Computed field about working time elapsed between record creation and assignation/closing. working_hours_open = fields.Float(compute='_compute_elapsed', string='Working hours to assign', store=True, group_operator="avg") working_hours_close = fields.Float(compute='_compute_elapsed', string='Working hours to close', store=True, group_operator="avg") working_days_open = fields.Float(compute='_compute_elapsed', string='Working days to assign', store=True, group_operator="avg") working_days_close = fields.Float(compute='_compute_elapsed', string='Working days to close', store=True, group_operator="avg") # customer portal: include comment and incoming emails in communication history website_message_ids = fields.One2many(domain=lambda self: [('model', '=', self._name), ('message_type', 'in', ['email', 'comment'])]) @api.depends('date_deadline') def _compute_date_deadline_formatted(self): for task in self: task.date_deadline_formatted = format_date(self.env, task.date_deadline) if task.date_deadline else None def _compute_attachment_ids(self): for task in self: attachment_ids = self.env['ir.attachment'].search([('res_id', '=', task.id), ('res_model', '=', 'project.task')]).ids message_attachment_ids = task.mapped('message_ids.attachment_ids').ids # from mail_thread task.attachment_ids = [(6, 0, list(set(attachment_ids) - set(message_attachment_ids)))] @api.depends('create_date', 'date_end', 'date_assign') def _compute_elapsed(self): task_linked_to_calendar = self.filtered( lambda task: task.project_id.resource_calendar_id and task.create_date ) for task in task_linked_to_calendar: dt_create_date = fields.Datetime.from_string(task.create_date) if task.date_assign: dt_date_assign = fields.Datetime.from_string(task.date_assign) duration_data = task.project_id.resource_calendar_id.get_work_duration_data(dt_create_date, dt_date_assign, compute_leaves=True) task.working_hours_open = duration_data['hours'] task.working_days_open = duration_data['days'] else: task.working_hours_open = 0.0 task.working_days_open = 0.0 if task.date_end: dt_date_end = fields.Datetime.from_string(task.date_end) duration_data = task.project_id.resource_calendar_id.get_work_duration_data(dt_create_date, dt_date_end, compute_leaves=True) task.working_hours_close = duration_data['hours'] task.working_days_close = duration_data['days'] else: task.working_hours_close = 0.0 task.working_days_close = 0.0 (self - task_linked_to_calendar).update(dict.fromkeys( ['working_hours_open', 'working_hours_close', 'working_days_open', 'working_days_close'], 0.0)) @api.depends('stage_id', 'kanban_state') def _compute_kanban_state_label(self): for task in self: if task.kanban_state == 'normal': task.kanban_state_label = task.legend_normal elif task.kanban_state == 'blocked': task.kanban_state_label = task.legend_blocked else: task.kanban_state_label = task.legend_done def _compute_access_url(self): super(Task, self)._compute_access_url() for task in self: task.access_url = '/my/task/%s' % task.id def _compute_access_warning(self): super(Task, self)._compute_access_warning() for task in self.filtered(lambda x: x.project_id.privacy_visibility != 'portal'): task.access_warning = _( "The task cannot be shared with the recipient(s) because the privacy of the project is too restricted. Set the privacy of the project to 'Visible by following customers' in order to make it accessible by the recipient(s).") @api.depends('child_ids.planned_hours') def _compute_subtask_planned_hours(self): for task in self: task.subtask_planned_hours = sum(task.child_ids.mapped('planned_hours')) @api.depends('child_ids') def _compute_subtask_count(self): """ Note: since we accept only one level subtask, we can use a read_group here """ task_data = self.env['project.task'].read_group([('parent_id', 'in', self.ids)], ['parent_id'], ['parent_id']) mapping = dict((data['parent_id'][0], data['parent_id_count']) for data in task_data) for task in self: task.subtask_count = mapping.get(task.id, 0) @api.onchange('partner_id') def _onchange_partner_id(self): self.email_from = self.partner_id.email @api.onchange('parent_id') def _onchange_parent_id(self): if self.parent_id: for field_name, value in self._subtask_values_from_parent(self.parent_id.id).items(): if not self[field_name]: self[field_name] = value @api.onchange('project_id') def _onchange_project(self): if self.project_id: # find partner if self.project_id.partner_id: self.partner_id = self.project_id.partner_id # find stage if self.project_id not in self.stage_id.project_ids: self.stage_id = self.stage_find(self.project_id.id, [('fold', '=', False)]) # keep multi company consistency self.company_id = self.project_id.company_id else: self.stage_id = False @api.constrains('parent_id', 'child_ids') def _check_subtask_level(self): for task in self: if task.parent_id and task.child_ids: raise ValidationError(_('Task %s cannot have several subtask levels.' % (task.name,))) @api.returns('self', lambda value: value.id) def copy(self, default=None): if default is None: default = {} if not default.get('name'): default['name'] = _("%s (copy)") % self.name return super(Task, self).copy(default) @api.constrains('parent_id') def _check_parent_id(self): for task in self: if not task._check_recursion(): raise ValidationError(_('Error! You cannot create recursive hierarchy of task(s).')) @api.model def get_empty_list_help(self, help): tname = _("task") project_id = self.env.context.get('default_project_id', False) if project_id: name = self.env['project.project'].browse(project_id).label_tasks if name: tname = name.lower() self = self.with_context( empty_list_help_id=self.env.context.get('default_project_id'), empty_list_help_model='project.project', empty_list_help_document_name=tname, ) return super(Task, self).get_empty_list_help(help) # ---------------------------------------- # Case management # ---------------------------------------- def stage_find(self, section_id, domain=[], order='sequence'): """ Override of the base.stage method Parameter of the stage search taken from the lead: - section_id: if set, stages must belong to this section or be a default stage; if not set, stages must be default stages """ # collect all section_ids section_ids = [] if section_id: section_ids.append(section_id) section_ids.extend(self.mapped('project_id').ids) search_domain = [] if section_ids: search_domain = [('|')] * (len(section_ids) - 1) for section_id in section_ids: search_domain.append(('project_ids', '=', section_id)) search_domain += list(domain) # perform search, return the first found return self.env['project.task.type'].search(search_domain, order=order, limit=1).id # ------------------------------------------------ # CRUD overrides # ------------------------------------------------ @api.model def create(self, vals): # context: no_log, because subtype already handle this context = dict(self.env.context) # for default stage if vals.get('project_id') and not context.get('default_project_id'): context['default_project_id'] = vals.get('project_id') # user_id change: update date_assign if vals.get('user_id'): vals['date_assign'] = fields.Datetime.now() # Stage change: Update date_end if folded stage and date_last_stage_update if vals.get('stage_id'): vals.update(self.update_date_end(vals['stage_id'])) vals['date_last_stage_update'] = fields.Datetime.now() # substask default values if vals.get('parent_id'): for fname, value in self._subtask_values_from_parent(vals['parent_id']).items(): if fname not in vals: vals[fname] = value task = super(Task, self.with_context(context)).create(vals) return task def write(self, vals): now = fields.Datetime.now() # stage change: update date_last_stage_update if 'stage_id' in vals: vals.update(self.update_date_end(vals['stage_id'])) vals['date_last_stage_update'] = now # reset kanban state when changing stage if 'kanban_state' not in vals: vals['kanban_state'] = 'normal' # user_id change: update date_assign if vals.get('user_id') and 'date_assign' not in vals: vals['date_assign'] = now result = super(Task, self).write(vals) # rating on stage if 'stage_id' in vals and vals.get('stage_id'): self.filtered(lambda x: x.project_id.rating_status == 'stage')._send_task_rating_mail(force_send=True) return result def update_date_end(self, stage_id): project_task_type = self.env['project.task.type'].browse(stage_id) if project_task_type.fold: return {'date_end': fields.Datetime.now()} return {'date_end': False} # --------------------------------------------------- # Subtasks # --------------------------------------------------- def _subtask_default_fields(self): """ Return the list of field name for default value when creating a subtask """ return ['partner_id', 'email_from'] def _subtask_values_from_parent(self, parent_id): """ Get values for substask implied field of the given""" result = {} parent_task = self.env['project.task'].browse(parent_id) for field_name in self._subtask_default_fields(): result[field_name] = parent_task[field_name] # special case for the subtask default project result['project_id'] = parent_task.project_id.subtask_project_id return self._convert_to_write(result) # --------------------------------------------------- # Mail gateway # --------------------------------------------------- def _track_template(self, changes): res = super(Task, self)._track_template(changes) test_task = self[0] if 'stage_id' in changes and test_task.stage_id.mail_template_id: res['stage_id'] = (test_task.stage_id.mail_template_id, { 'auto_delete_message': True, 'subtype_id': self.env['ir.model.data'].xmlid_to_res_id('mail.mt_note'), 'email_layout_xmlid': 'mail.mail_notification_light' }) return res def _creation_subtype(self): return self.env.ref('project.mt_task_new') def _track_subtype(self, init_values): self.ensure_one() if 'kanban_state_label' in init_values and self.kanban_state == 'blocked': return self.env.ref('project.mt_task_blocked') elif 'kanban_state_label' in init_values and self.kanban_state == 'done': return self.env.ref('project.mt_task_ready') elif 'stage_id' in init_values: return self.env.ref('project.mt_task_stage') return super(Task, self)._track_subtype(init_values) def _notify_get_groups(self): """ Handle project users and managers recipients that can assign tasks and create new one directly from notification emails. Also give access button to portal users and portal customers. If they are notified they should probably have access to the document. """ groups = super(Task, self)._notify_get_groups() self.ensure_one() project_user_group_id = self.env.ref('project.group_project_user').id new_group = ( 'group_project_user', lambda pdata: pdata['type'] == 'user' and project_user_group_id in pdata['groups'], {}, ) if not self.user_id and not self.stage_id.fold: take_action = self._notify_get_action_link('assign') project_actions = [{'url': take_action, 'title': _('I take it')}] new_group[2]['actions'] = project_actions groups = [new_group] + groups for group_name, group_method, group_data in groups: if group_name != 'customer': group_data['has_button_access'] = True return groups def _notify_get_reply_to(self, default=None, records=None, company=None, doc_names=None): """ Override to set alias of tasks to their project if any. """ aliases = self.sudo().mapped('project_id')._notify_get_reply_to(default=default, records=None, company=company, doc_names=None) res = {task.id: aliases.get(task.project_id.id) for task in self} leftover = self.filtered(lambda rec: not rec.project_id) if leftover: res.update(super(Task, leftover)._notify_get_reply_to(default=default, records=None, company=company, doc_names=doc_names)) return res def email_split(self, msg): email_list = tools.email_split((msg.get('to') or '') + ',' + (msg.get('cc') or '')) # check left-part is not already an alias aliases = self.mapped('project_id.alias_name') return [x for x in email_list if x.split('@')[0] not in aliases] @api.model def message_new(self, msg, custom_values=None): """ Overrides mail_thread message_new that is called by the mailgateway through message_process. This override updates the document according to the email. """ # remove default author when going through the mail gateway. Indeed we # do not want to explicitly set user_id to False; however we do not # want the gateway user to be responsible if no other responsible is # found. create_context = dict(self.env.context or {}) create_context['default_user_id'] = False if custom_values is None: custom_values = {} defaults = { 'name': msg.get('subject') or _("No Subject"), 'email_from': msg.get('from'), 'planned_hours': 0.0, 'partner_id': msg.get('author_id') } defaults.update(custom_values) task = super(Task, self.with_context(create_context)).message_new(msg, custom_values=defaults) email_list = task.email_split(msg) partner_ids = [p.id for p in self.env['mail.thread']._mail_find_partner_from_emails(email_list, records=task, force_create=False) if p] task.message_subscribe(partner_ids) return task def message_update(self, msg, update_vals=None): """ Override to update the task according to the email. """ email_list = self.email_split(msg) partner_ids = [p.id for p in self.env['mail.thread']._mail_find_partner_from_emails(email_list, records=self, force_create=False) if p] self.message_subscribe(partner_ids) return super(Task, self).message_update(msg, update_vals=update_vals) def _message_get_suggested_recipients(self): recipients = super(Task, self)._message_get_suggested_recipients() for task in self: if task.partner_id: reason = _('Customer Email') if task.partner_id.email else _('Customer') task._message_add_suggested_recipient(recipients, partner=task.partner_id, reason=reason) elif task.email_from: task._message_add_suggested_recipient(recipients, email=task.email_from, reason=_('Customer Email')) return recipients def _notify_email_header_dict(self): headers = super(Task, self)._notify_email_header_dict() if self.project_id: current_objects = [h for h in headers.get('X-Eagle-Objects', '').split(',') if h] current_objects.insert(0, 'project.project-%s, ' % self.project_id.id) headers['X-Eagle-Objects'] = ','.join(current_objects) if self.tag_ids: headers['X-Eagle-Tags'] = ','.join(self.tag_ids.mapped('name')) return headers def _message_post_after_hook(self, message, msg_vals): if self.email_from and not self.partner_id: # we consider that posting a message with a specified recipient (not a follower, a specific one) # on a document without customer means that it was created through the chatter using # suggested recipients. This heuristic allows to avoid ugly hacks in JS. new_partner = message.partner_ids.filtered(lambda partner: partner.email == self.email_from) if new_partner: self.search([ ('partner_id', '=', False), ('email_from', '=', new_partner.email), ('stage_id.fold', '=', False)]).write({'partner_id': new_partner.id}) return super(Task, self)._message_post_after_hook(message, msg_vals) def action_assign_to_me(self): self.write({'user_id': self.env.user.id}) def action_open_parent_task(self): return { 'name': _('Parent Task'), 'view_mode': 'form', 'res_model': 'project.task', 'res_id': self.parent_id.id, 'type': 'ir.actions.act_window', 'context': dict(self._context, create=False) } def action_subtask(self): action = self.env.ref('project.project_task_action_sub_task').read()[0] # only display subtasks of current task action['domain'] = [('id', 'child_of', self.id), ('id', '!=', self.id)] # update context, with all default values as 'quick_create' does not contains all field in its view if self._context.get('default_project_id'): default_project = self.env['project.project'].browse(self.env.context['default_project_id']) else: default_project = self.project_id.subtask_project_id or self.project_id ctx = dict(self.env.context) ctx.update({ 'default_name': self.env.context.get('name', self.name) + ':', 'default_parent_id': self.id, # will give default subtask field in `default_get` 'default_company_id': default_project.company_id.id if default_project else self.env.company.id, 'search_default_parent_id': self.id, }) parent_values = self._subtask_values_from_parent(self.id) for fname, value in parent_values.items(): if 'default_' + fname not in ctx: ctx['default_' + fname] = value action['context'] = ctx return action # --------------------------------------------------- # Rating business # --------------------------------------------------- def _send_task_rating_mail(self, force_send=False): for task in self: rating_template = task.stage_id.rating_template_id if rating_template: task.rating_send_request(rating_template, lang=task.partner_id.lang, force_send=force_send) def rating_get_partner_id(self): res = super(Task, self).rating_get_partner_id() if not res and self.project_id.partner_id: return self.project_id.partner_id return res def rating_apply(self, rate, token=None, feedback=None, subtype=None): return super(Task, self).rating_apply(rate, token=token, feedback=feedback, subtype="project.mt_task_rating") def _rating_get_parent_field_name(self): return 'project_id'
class AccountInvoiceSend(models.TransientModel): _name = 'account.invoice.send' _inherit = 'account.invoice.send' _description = 'Account Invoice Send' partner_id = fields.Many2one('res.partner', compute='_get_partner', string='Partner') snailmail_is_letter = fields.Boolean('Send by Post', help='Allows to send the document by Snailmail (coventional posting delivery service)', default=lambda self: self.env.company.invoice_is_snailmail) snailmail_cost = fields.Float(string='Stamp(s)', compute='_compute_snailmail_cost', readonly=True) invalid_addresses = fields.Integer('Invalid Addresses Count', compute='_compute_invalid_addresses') invalid_invoice_ids = fields.Many2many('account.move', string='Invalid Addresses', compute='_compute_invalid_addresses') @api.depends('invoice_ids') def _compute_invalid_addresses(self): for wizard in self: invalid_invoices = wizard.invoice_ids.filtered(lambda i: not self.env['snailmail.letter']._is_valid_address(i.partner_id)) wizard.invalid_invoice_ids = invalid_invoices wizard.invalid_addresses = len(invalid_invoices) @api.depends('invoice_ids') def _get_partner(self): self.partner_id = self.env['res.partner'] for wizard in self: if wizard.invoice_ids and len(wizard.invoice_ids) == 1: wizard.partner_id = wizard.invoice_ids.partner_id.id @api.depends('snailmail_is_letter') def _compute_snailmail_cost(self): for wizard in self: wizard.snailmail_cost = len(wizard.invoice_ids.ids) def snailmail_print_action(self): self.ensure_one() letters = self.env['snailmail.letter'] for invoice in self.invoice_ids: letter = self.env['snailmail.letter'].create({ 'partner_id': invoice.partner_id.id, 'model': 'account.move', 'res_id': invoice.id, 'user_id': self.env.user.id, 'company_id': invoice.company_id.id, 'report_template': self.env.ref('account.account_invoices').id }) letters |= letter self.invoice_ids.filtered(lambda inv: not inv.invoice_sent).write({'invoice_sent': True}) if len(self.invoice_ids) == 1: letters._snailmail_print() else: letters._snailmail_print(immediate=False) def send_and_print_action(self): if self.snailmail_is_letter: if self.invalid_addresses and self.composition_mode == "mass_mail": self.notify_invalid_addresses() self.snailmail_print_action() res = super(AccountInvoiceSend, self).send_and_print_action() return res def notify_invalid_addresses(self): self.ensure_one() self.env['bus.bus'].sendone( (self._cr.dbname, 'res.partner', self.env.user.partner_id.id), {'type': 'snailmail_invalid_address', 'title': _("Invalid Addresses"), 'message': _("%s of the selected invoice(s) had an invalid address and were not sent") % self.invalid_addresses} ) def invalid_addresses_action(self): return { 'name': _('Invalid Addresses'), 'type': 'ir.actions.act_window', 'view_mode': 'kanban,tree,form', 'res_model': 'account.move', 'domain': [('id', 'in', self.mapped('invalid_invoice_ids').ids)], }
class Task(models.Model): _inherit = "project.task" analytic_account_active = fields.Boolean("Analytic Account", related='project_id.analytic_account_id.active', readonly=True) allow_timesheets = fields.Boolean("Allow timesheets", related='project_id.allow_timesheets', help="Timesheets can be logged on this task.", readonly=True) remaining_hours = fields.Float("Remaining Hours", compute='_compute_remaining_hours', store=True, readonly=True, help="Total remaining time, can be re-estimated periodically by the assignee of the task.") effective_hours = fields.Float("Hours Spent", compute='_compute_effective_hours', compute_sudo=True, store=True, help="Computed using the sum of the task work done.") total_hours_spent = fields.Float("Total Hours", compute='_compute_total_hours_spent', store=True, help="Computed as: Time Spent + Sub-tasks Hours.") progress = fields.Float("Progress", compute='_compute_progress_hours', store=True, group_operator="avg", help="Display progress of current task.") subtask_effective_hours = fields.Float("Sub-tasks Hours Spent", compute='_compute_subtask_effective_hours', store=True, help="Sum of actually spent hours on the subtask(s)", oldname='children_hours') timesheet_ids = fields.One2many('account.analytic.line', 'task_id', 'Timesheets') @api.depends('timesheet_ids.unit_amount') def _compute_effective_hours(self): for task in self: task.effective_hours = round(sum(task.timesheet_ids.mapped('unit_amount')), 2) @api.depends('effective_hours', 'subtask_effective_hours', 'planned_hours') def _compute_progress_hours(self): for task in self: if (task.planned_hours > 0.0): task_total_hours = task.effective_hours + task.subtask_effective_hours if task_total_hours > task.planned_hours: task.progress = 100 else: task.progress = round(100.0 * task_total_hours / task.planned_hours, 2) else: task.progress = 0.0 @api.depends('effective_hours', 'subtask_effective_hours', 'planned_hours') def _compute_remaining_hours(self): for task in self: task.remaining_hours = task.planned_hours - task.effective_hours - task.subtask_effective_hours @api.depends('effective_hours', 'subtask_effective_hours') def _compute_total_hours_spent(self): for task in self: task.total_hours_spent = task.effective_hours + task.subtask_effective_hours @api.depends('child_ids.effective_hours', 'child_ids.subtask_effective_hours') def _compute_subtask_effective_hours(self): for task in self: task.subtask_effective_hours = sum(child_task.effective_hours + child_task.subtask_effective_hours for child_task in task.child_ids) # --------------------------------------------------------- # ORM # --------------------------------------------------------- @api.multi def write(self, values): # a timesheet must have an analytic account (and a project) if 'project_id' in values and self and not values.get('project_id'): raise UserError(_('This task must be part of a project because they some timesheets are linked to it.')) return super(Task, self).write(values) @api.model def _fields_view_get(self, view_id=None, view_type='form', toolbar=False, submenu=False): """ Set the correct label for `unit_amount`, depending on company UoM """ result = super(Task, self)._fields_view_get(view_id=view_id, view_type=view_type, toolbar=toolbar, submenu=submenu) result['arch'] = self.env['account.analytic.line']._apply_timesheet_label(result['arch']) return result
class pay_manually_wiz(models.TransientModel): _name = 'tc.fee.pay.manually.wiz' amount = fields.Float(string='Amount') journal_id = fields.Many2one('account.journal', string="Payment Method") cheque = fields.Boolean(string='Cheque') jounral_id_store = fields.Char(string='Jounral Store') cheque_start_date = fields.Date('Cheque Start Date') cheque_expiry_date = fields.Date('Cheque Expiry Date') bank_name = fields.Char('Bank Name') party_name = fields.Char('Party Name') chk_num = fields.Char('Cheque Number') label_change = fields.Boolean(string="Label Change") @api.onchange('journal_id') def store_jounral(self): self.cheque = self.journal_id.is_cheque @api.model def default_get(self, fields): id = self._context.get('active_id') res = super(pay_manually_wiz, self).default_get(fields) student_tc_rec = self.env['trensfer.certificate'].browse(id) res['amount'] = student_tc_rec.credit + student_tc_rec.parent_credit return res @api.model def _get_period(self): if self._context is None: context = {} if self._context.get('period_id', False): return self._context.get('period_id') periods = self.env['account.period'].search([]) return periods and periods[0] or False @api.model def _get_currency(self): if self._context is None: self._context = {} journal_pool = self.env['account.journal'] journal_id = self._context.get('journal_id', False) if journal_id: if isinstance(journal_id, (list, tuple)): # sometimes journal_id is a pair (id, display_name) journal_id = journal_id[0] journal = journal_pool.browse(journal_id) if journal.currency: return journal.currency.id return self.env['res.users'].browse( self._uid).company_id.currency_id.id @api.one def submit_fee(self): print '=======3333333333333333=submit fee=========' """ :return: """ active_id = self._context['active_ids'] print(active_id, '==========active_id ----amount----->>>', self.amount) trensfer_certificate_obj = self.env['trensfer.certificate'] account_payment_obj = self.env['account.payment'] payment_vals = {} for trensfer_certificate_rec in trensfer_certificate_obj.browse( active_id): if self.amount > 0.00: payment_vals = self.get_payment_vals('customer', 'inbound', trensfer_certificate_rec) if self.amount < 0.00: payment_vals = self.get_payment_vals('supplier', 'outbound', trensfer_certificate_rec) trensfer_certificate_rec.credit -= self.amount if payment_vals: payment_rec = account_payment_obj.create(payment_vals) payment_rec.post() print(payment_rec, trensfer_certificate_rec.credit, trensfer_certificate_rec.state, '==============payment_rec===---vals ---> ') # print(paymment_vals) def get_payment_vals(self, partner_type, payment_type, trensfer_certificate_rec): if abs(self.amount) > 0.00: payment_vals = { # 'period_id': period_id, 'partner_type': partner_type, 'partner_id': trensfer_certificate_rec.name.id, 'journal_id': self.journal_id.id, 'amount': abs(self.amount), 'payment_method_id': 1, 'payment_type': payment_type, } return payment_vals # @api.one # def submit_fee(self): # print '========submit fee=========' # """ # :return: # """ # active_id = self._context['active_ids'] # trensfer_certificate_obj = self.env['trensfer.certificate'] # account_voucher_obj = self.env['account.voucher'] # voucher_line_obj = self.env['account.voucher.line'] # for trensfer_certificate_rec in trensfer_certificate_obj.browse(active_id): # if self.amount > 0.00: # period_rec = self._get_period() # curency_id = self._get_currency() # vouch_sequence = self.env['ir.sequence'].get('voucher.payfort') or '/' # voucher_data = { # 'period_id': period_rec.id, # 'journal_id': self.journal_id.id, # 'account_id': self.journal_id.default_debit_account_id.id, # 'partner_id': trensfer_certificate_rec.name.id, # 'currency_id': curency_id, # 'reference': trensfer_certificate_rec.code, # 'amount': self.amount, # 'type': 'receipt' or 'payment', # 'state': 'draft', # 'pay_now': 'pay_later', # 'name': trensfer_certificate_rec.code, # 'date': time.strftime('%Y-%m-%d'), # 'company_id': 1, # 'tax_id': False, # 'payment_option': 'without_writeoff', # 'comment': _('Write-Off'), # 'payfort_type': True, # 'payfort_link_order_id' : vouch_sequence, # 'cheque_start_date':self.cheque_start_date, # 'cheque_expiry_date':self.cheque_expiry_date, # 'bank_name':self.bank_name, # 'cheque':self.cheque, # 'party_name' :self.party_name, # 'chk_num':self.chk_num, # 'voucher_type':'sale' or 'purchase', # } # print voucher_data,'====================voucher_data' # voucher_rec = account_voucher_obj.create(voucher_data) # print voucher_rec,'=====================voucher rec' # date = time.strftime('%Y-%m-%d') # res = voucher_rec.onchange_partner_id(voucher_rec.partner_id.id, self.journal_id.id, # self.amount, # voucher_rec.currency_id.id, # voucher_rec.type, date) # print res,'----------------res-----' # for line_data in res['value']['line_cr_ids']: # print line_data,'====================== line data ============' # voucher_lines = { # 'move_line_id': line_data['move_line_id'], # 'amount': line_data['amount_unreconciled'] or line_data['amount'], # 'name': line_data['name'], # 'amount_unreconciled': line_data['amount_unreconciled'], # 'type': line_data['type'], # 'amount_original': line_data['amount_original'], # 'account_id': line_data['account_id'], # 'voucher_id': voucher_rec.id, # 'reconcile': True # } # print voucher_lines,'====================---------------------voucher_lines pay manualy-' # self.env['account.voucher.line'].sudo().create(voucher_lines) # # # Validate voucher (Add Journal Entries) # voucher_rec.proforma_voucher() # trensfer_certificate_rec.send_fee_receipt_mail(voucher_rec) # # if self.amount < 0.00: # period_rec = self._get_period() # curency_id = self._get_currency() # vouch_sequence = self.env['ir.sequence'].get('voucher.payfort') or '/' # voucher_data = { # 'period_id': period_rec.id, # 'journal_id': self.journal_id.id, # 'account_id': self.journal_id.default_debit_account_id.id, # 'partner_id': trensfer_certificate_rec.name.id, # 'currency_id': curency_id, # 'reference': trensfer_certificate_rec.name.name, # 'amount': self.amount, # 'type': 'receipt' or 'payment', # 'state': 'draft', # 'pay_now': 'pay_later', # 'name': '', # 'date': time.strftime('%Y-%m-%d'), # 'company_id': 1, # 'tax_id': False, # 'payment_option': 'without_writeoff', # 'comment': _('Write-Off'), # 'payfort_type': True, # 'payfort_link_order_id': vouch_sequence, # 'cheque_start_date':self.cheque_start_date, # 'cheque_expiry_date':self.cheque_expiry_date, # 'bank_name':self.bank_name, # 'cheque':self.cheque, # 'party_name' :self.party_name, # 'chk_num':self.chk_num, # } # print voucher_data,'1111111111111111111111111 voucher idata' # voucher_rec = account_voucher_obj.create(voucher_data) # date = time.strftime('%Y-%m-%d') # res = voucher_rec.onchange_partner_id(voucher_rec.partner_id.id, self.journal_id.id, # self.amount, # voucher_rec.currency_id.id, # voucher_rec.type, date) # print res,'===============5555555555555555555555555 voucher rec' # for line_data in res['value']['line_dr_ids']: # voucher_lines = { # 'move_line_id': line_data['move_line_id'], # 'amount': line_data['amount_unreconciled'] or line_data['amount'], # 'name': line_data['name'], # 'amount_unreconciled': line_data['amount_unreconciled'], # 'type': line_data['type'], # 'amount_original': line_data['amount_original'], # 'account_id': line_data['account_id'], # 'voucher_id': voucher_rec.id, # 'reconcile': True # } # print voucher_lines,'99999999999999999999999 voucher lines' # voucher_line_obj.sudo().create(voucher_lines) # # # Validate voucher (Add Journal Entries) # voucher_rec.proforma_voucher()
class StockQuant(models.Model): _name = 'stock.quant' _description = 'Quants' _rec_name = 'product_id' product_id = fields.Many2one('product.product', 'Product', ondelete='restrict', readonly=True, required=True) # so user can filter on template in webclient product_tmpl_id = fields.Many2one('product.template', string='Product Template', related='product_id.product_tmpl_id', readonly=False) product_uom_id = fields.Many2one('uom.uom', 'Unit of Measure', readonly=True, related='product_id.uom_id') company_id = fields.Many2one(related='location_id.company_id', string='Company', store=True, readonly=True) location_id = fields.Many2one('stock.location', 'Location', auto_join=True, ondelete='restrict', readonly=True, required=True) lot_id = fields.Many2one('stock.production.lot', 'Lot/Serial Number', ondelete='restrict', readonly=True) package_id = fields.Many2one('stock.quant.package', 'Package', help='The package containing this quant', readonly=True, ondelete='restrict') owner_id = fields.Many2one('res.partner', 'Owner', help='This is the owner of the quant', readonly=True) quantity = fields.Float( 'Quantity', help= 'Quantity of products in this quant, in the default unit of measure of the product', readonly=True, required=True, oldname='qty') reserved_quantity = fields.Float( 'Reserved Quantity', default=0.0, help= 'Quantity of reserved products in this quant, in the default unit of measure of the product', readonly=True, required=True) in_date = fields.Datetime('Incoming Date', readonly=True) def action_view_stock_moves(self): self.ensure_one() action = self.env.ref('stock.stock_move_line_action').read()[0] action['domain'] = [ ('product_id', '=', self.product_id.id), '|', ('location_id', '=', self.location_id.id), ('location_dest_id', '=', self.location_id.id), ('lot_id', '=', self.lot_id.id), '|', ('package_id', '=', self.package_id.id), ('result_package_id', '=', self.package_id.id), ] return action @api.constrains('product_id') def check_product_id(self): if any(elem.product_id.type != 'product' for elem in self): raise ValidationError( _('Quants cannot be created for consumables or services.')) @api.constrains('quantity') def check_quantity(self): for quant in self: if float_compare( quant.quantity, 1, precision_rounding=quant.product_uom_id.rounding ) > 0 and quant.lot_id and quant.product_id.tracking == 'serial': raise ValidationError( _('A serial number should only be linked to a single product.' )) @api.constrains('location_id') def check_location_id(self): for quant in self: if quant.location_id.usage == 'view': raise ValidationError( _('You cannot take products from or deliver products to a location of type "view".' )) @api.one def _compute_name(self): self.name = '%s: %s%s' % (self.lot_id.name or self.product_id.code or '', self.quantity, self.product_id.uom_id.name) @api.model def _get_removal_strategy(self, product_id, location_id): if product_id.categ_id.removal_strategy_id: return product_id.categ_id.removal_strategy_id.method loc = location_id while loc: if loc.removal_strategy_id: return loc.removal_strategy_id.method loc = loc.location_id return 'fifo' @api.model def _get_removal_strategy_order(self, removal_strategy): if removal_strategy == 'fifo': return 'in_date ASC NULLS FIRST, id' elif removal_strategy == 'lifo': return 'in_date DESC NULLS LAST, id desc' raise UserError( _('Removal strategy %s not implemented.') % (removal_strategy, )) def _gather(self, product_id, location_id, lot_id=None, package_id=None, owner_id=None, strict=False): removal_strategy = self._get_removal_strategy(product_id, location_id) removal_strategy_order = self._get_removal_strategy_order( removal_strategy) domain = [ ('product_id', '=', product_id.id), ] if not strict: if lot_id: domain = expression.AND([[('lot_id', '=', lot_id.id)], domain]) if package_id: domain = expression.AND([[('package_id', '=', package_id.id)], domain]) if owner_id: domain = expression.AND([[('owner_id', '=', owner_id.id)], domain]) domain = expression.AND([[('location_id', 'child_of', location_id.id)], domain]) else: domain = expression.AND([[('lot_id', '=', lot_id and lot_id.id or False)], domain]) domain = expression.AND([[ ('package_id', '=', package_id and package_id.id or False) ], domain]) domain = expression.AND([[ ('owner_id', '=', owner_id and owner_id.id or False) ], domain]) domain = expression.AND([[('location_id', '=', location_id.id)], domain]) # Copy code of _search for special NULLS FIRST/LAST order self.sudo(self._uid).check_access_rights('read') query = self._where_calc(domain) self._apply_ir_rules(query, 'read') from_clause, where_clause, where_clause_params = query.get_sql() where_str = where_clause and (" WHERE %s" % where_clause) or '' query_str = 'SELECT "%s".id FROM ' % self._table + from_clause + where_str + " ORDER BY " + removal_strategy_order self._cr.execute(query_str, where_clause_params) res = self._cr.fetchall() # No uniquify list necessary as auto_join is not applied anyways... return self.browse([x[0] for x in res]) @api.model def _get_available_quantity(self, product_id, location_id, lot_id=None, package_id=None, owner_id=None, strict=False, allow_negative=False): """ Return the available quantity, i.e. the sum of `quantity` minus the sum of `reserved_quantity`, for the set of quants sharing the combination of `product_id, location_id` if `strict` is set to False or sharing the *exact same characteristics* otherwise. This method is called in the following usecases: - when a stock move checks its availability - when a stock move actually assign - when editing a move line, to check if the new value is forced or not - when validating a move line with some forced values and have to potentially unlink an equivalent move line in another picking In the two first usecases, `strict` should be set to `False`, as we don't know what exact quants we'll reserve, and the characteristics are meaningless in this context. In the last ones, `strict` should be set to `True`, as we work on a specific set of characteristics. :return: available quantity as a float """ self = self.sudo() quants = self._gather(product_id, location_id, lot_id=lot_id, package_id=package_id, owner_id=owner_id, strict=strict) rounding = product_id.uom_id.rounding if product_id.tracking == 'none': available_quantity = sum(quants.mapped('quantity')) - sum( quants.mapped('reserved_quantity')) if allow_negative: return available_quantity else: return available_quantity if float_compare( available_quantity, 0.0, precision_rounding=rounding) >= 0.0 else 0.0 else: availaible_quantities = { lot_id: 0.0 for lot_id in list(set(quants.mapped('lot_id'))) + ['untracked'] } for quant in quants: if not quant.lot_id: availaible_quantities[ 'untracked'] += quant.quantity - quant.reserved_quantity else: availaible_quantities[ quant. lot_id] += quant.quantity - quant.reserved_quantity if allow_negative: return sum(availaible_quantities.values()) else: return sum([ available_quantity for available_quantity in availaible_quantities.values() if float_compare( available_quantity, 0, precision_rounding=rounding) > 0 ]) @api.model def _update_available_quantity(self, product_id, location_id, quantity, lot_id=None, package_id=None, owner_id=None, in_date=None): """ Increase or decrease `reserved_quantity` of a set of quants for a given set of product_id/location_id/lot_id/package_id/owner_id. :param product_id: :param location_id: :param quantity: :param lot_id: :param package_id: :param owner_id: :param datetime in_date: Should only be passed when calls to this method are done in order to move a quant. When creating a tracked quant, the current datetime will be used. :return: tuple (available_quantity, in_date as a datetime) """ self = self.sudo() quants = self._gather(product_id, location_id, lot_id=lot_id, package_id=package_id, owner_id=owner_id, strict=True) incoming_dates = [d for d in quants.mapped('in_date') if d] incoming_dates = [ fields.Datetime.from_string(incoming_date) for incoming_date in incoming_dates ] if in_date: incoming_dates += [in_date] # If multiple incoming dates are available for a given lot_id/package_id/owner_id, we # consider only the oldest one as being relevant. if incoming_dates: in_date = fields.Datetime.to_string(min(incoming_dates)) else: in_date = fields.Datetime.now() for quant in quants: try: with self._cr.savepoint(): self._cr.execute( "SELECT 1 FROM stock_quant WHERE id = %s FOR UPDATE NOWAIT", [quant.id], log_exceptions=False) quant.write({ 'quantity': quant.quantity + quantity, 'in_date': in_date, }) break except OperationalError as e: if e.pgcode == '55P03': # could not obtain the lock continue else: raise else: self.create({ 'product_id': product_id.id, 'location_id': location_id.id, 'quantity': quantity, 'lot_id': lot_id and lot_id.id, 'package_id': package_id and package_id.id, 'owner_id': owner_id and owner_id.id, 'in_date': in_date, }) return self._get_available_quantity( product_id, location_id, lot_id=lot_id, package_id=package_id, owner_id=owner_id, strict=False, allow_negative=True), fields.Datetime.from_string(in_date) @api.model def _update_reserved_quantity(self, product_id, location_id, quantity, lot_id=None, package_id=None, owner_id=None, strict=False): """ Increase the reserved quantity, i.e. increase `reserved_quantity` for the set of quants sharing the combination of `product_id, location_id` if `strict` is set to False or sharing the *exact same characteristics* otherwise. Typically, this method is called when reserving a move or updating a reserved move line. When reserving a chained move, the strict flag should be enabled (to reserve exactly what was brought). When the move is MTS,it could take anything from the stock, so we disable the flag. When editing a move line, we naturally enable the flag, to reflect the reservation according to the edition. :return: a list of tuples (quant, quantity_reserved) showing on which quant the reservation was done and how much the system was able to reserve on it """ self = self.sudo() rounding = product_id.uom_id.rounding quants = self._gather(product_id, location_id, lot_id=lot_id, package_id=package_id, owner_id=owner_id, strict=strict) reserved_quants = [] if float_compare(quantity, 0, precision_rounding=rounding) > 0: # if we want to reserve available_quantity = self._get_available_quantity( product_id, location_id, lot_id=lot_id, package_id=package_id, owner_id=owner_id, strict=strict) if float_compare(quantity, available_quantity, precision_rounding=rounding) > 0: raise UserError( _('It is not possible to reserve more products of %s than you have in stock.' ) % product_id.display_name) elif float_compare(quantity, 0, precision_rounding=rounding) < 0: # if we want to unreserve available_quantity = sum(quants.mapped('reserved_quantity')) if float_compare(abs(quantity), available_quantity, precision_rounding=rounding) > 0: raise UserError( _('It is not possible to unreserve more products of %s than you have in stock.' ) % product_id.display_name) else: return reserved_quants for quant in quants: if float_compare(quantity, 0, precision_rounding=rounding) > 0: max_quantity_on_quant = quant.quantity - quant.reserved_quantity if float_compare(max_quantity_on_quant, 0, precision_rounding=rounding) <= 0: continue max_quantity_on_quant = min(max_quantity_on_quant, quantity) quant.reserved_quantity += max_quantity_on_quant reserved_quants.append((quant, max_quantity_on_quant)) quantity -= max_quantity_on_quant available_quantity -= max_quantity_on_quant else: max_quantity_on_quant = min(quant.reserved_quantity, abs(quantity)) quant.reserved_quantity -= max_quantity_on_quant reserved_quants.append((quant, -max_quantity_on_quant)) quantity += max_quantity_on_quant available_quantity += max_quantity_on_quant if float_is_zero( quantity, precision_rounding=rounding) or float_is_zero( available_quantity, precision_rounding=rounding): break return reserved_quants @api.model def _unlink_zero_quants(self): """ _update_available_quantity may leave quants with no quantity and no reserved_quantity. It used to directly unlink these zero quants but this proved to hurt the performance as this method is often called in batch and each unlink invalidate the cache. We defer the calls to unlink in this method. """ precision_digits = max( 6, self.env.ref('product.decimal_product_uom').digits * 2) # Use a select instead of ORM search for UoM robustness. query = """SELECT id FROM stock_quant WHERE round(quantity::numeric, %s) = 0 AND round(reserved_quantity::numeric, %s) = 0;""" params = (precision_digits, precision_digits) self.env.cr.execute(query, params) quant_ids = self.env['stock.quant'].browse( [quant['id'] for quant in self.env.cr.dictfetchall()]) quant_ids.sudo().unlink() @api.model def _merge_quants(self): """ In a situation where one transaction is updating a quant via `_update_available_quantity` and another concurrent one calls this function with the same argument, we’ll create a new quant in order for these transactions to not rollback. This method will find and deduplicate these quants. """ query = """WITH dupes AS ( SELECT min(id) as to_update_quant_id, (array_agg(id ORDER BY id))[2:array_length(array_agg(id), 1)] as to_delete_quant_ids, SUM(reserved_quantity) as reserved_quantity, SUM(quantity) as quantity FROM stock_quant GROUP BY product_id, company_id, location_id, lot_id, package_id, owner_id, in_date HAVING count(id) > 1 ), _up AS ( UPDATE stock_quant q SET quantity = d.quantity, reserved_quantity = d.reserved_quantity FROM dupes d WHERE d.to_update_quant_id = q.id ) DELETE FROM stock_quant WHERE id in (SELECT unnest(to_delete_quant_ids) from dupes) """ try: with self.env.cr.savepoint(): self.env.cr.execute(query) except Error as e: _logger.info('an error occured while merging quants: %s', e.pgerror)
class StockMove(models.Model): _inherit = "stock.move" to_refund = fields.Boolean( string="To Refund (update SO/PO)", copy=False, help= 'Trigger a decrease of the delivered/received quantity in the associated Sale Order/Purchase Order' ) value = fields.Float(copy=False) remaining_qty = fields.Float(copy=False) remaining_value = fields.Float(copy=False) account_move_ids = fields.One2many('account.move', 'stock_move_id') @api.multi def action_get_account_moves(self): self.ensure_one() action_ref = self.env.ref('account.action_move_journal_line') if not action_ref: return False action_data = action_ref.read()[0] action_data['domain'] = [('id', 'in', self.account_move_ids.ids)] return action_data def _get_price_unit(self): """ Returns the unit price to store on the quant """ return not self.company_id.currency_id.is_zero( self.price_unit ) and self.price_unit or self.product_id.standard_price @api.model def _get_in_base_domain(self, company_id=False): # Domain: # - state is done # - coming from a location without company, or an inventory location within the same company # - going to a location within the same company domain = [ ('state', '=', 'done'), '&', '|', ('location_id.company_id', '=', False), '&', ('location_id.usage', 'in', ['inventory', 'production']), ('location_id.company_id', '=', company_id or self.env.user.company_id.id), ('location_dest_id.company_id', '=', company_id or self.env.user.company_id.id), ] return domain @api.model def _get_all_base_domain(self, company_id=False): # Domain: # - state is done # Then, either 'in' or 'out' moves. # 'in' moves: # - coming from a location without company, or an inventory location within the same company # - going to a location within the same company # 'out' moves: # - coming from to a location within the same company # - going to a location without company, or an inventory location within the same company domain = [ ('state', '=', 'done'), '|', '&', '|', ('location_id.company_id', '=', False), '&', ('location_id.usage', 'in', ['inventory', 'production']), ('location_id.company_id', '=', company_id or self.env.user.company_id.id), ('location_dest_id.company_id', '=', company_id or self.env.user.company_id.id), '&', ('location_id.company_id', '=', company_id or self.env.user.company_id.id), '|', ('location_dest_id.company_id', '=', False), '&', ('location_dest_id.usage', 'in', ['inventory', 'production']), ('location_dest_id.company_id', '=', company_id or self.env.user.company_id.id), ] return domain def _get_in_domain(self): return [('product_id', '=', self.product_id.id) ] + self._get_in_base_domain(company_id=self.company_id.id) def _get_all_domain(self): return [('product_id', '=', self.product_id.id) ] + self._get_all_base_domain(company_id=self.company_id.id) def _is_in(self): """ Check if the move should be considered as entering the company so that the cost method will be able to apply the correct logic. :return: True if the move is entering the company else False """ for move_line in self.move_line_ids.filtered( lambda ml: not ml.owner_id): if not move_line.location_id._should_be_valued( ) and move_line.location_dest_id._should_be_valued(): return True return False def _is_out(self): """ Check if the move should be considered as leaving the company so that the cost method will be able to apply the correct logic. :return: True if the move is leaving the company else False """ for move_line in self.move_line_ids.filtered( lambda ml: not ml.owner_id): if move_line.location_id._should_be_valued( ) and not move_line.location_dest_id._should_be_valued(): return True return False def _is_dropshipped(self): """ Check if the move should be considered as a dropshipping move so that the cost method will be able to apply the correct logic. :return: True if the move is a dropshipping one else False """ return self.location_id.usage == 'supplier' and self.location_dest_id.usage == 'customer' def _is_dropshipped_returned(self): """ Check if the move should be considered as a returned dropshipping move so that the cost method will be able to apply the correct logic. :return: True if the move is a returned dropshipping one else False """ return self.location_id.usage == 'customer' and self.location_dest_id.usage == 'supplier' @api.model def _run_fifo(self, move, quantity=None): """ Value `move` according to the FIFO rule, meaning we consume the oldest receipt first. Candidates receipts are marked consumed or free thanks to their `remaining_qty` and `remaining_value` fields. By definition, `move` should be an outgoing stock move. :param quantity: quantity to value instead of `move.product_qty` :returns: valued amount in absolute """ move.ensure_one() # Deal with possible move lines that do not impact the valuation. valued_move_lines = move.move_line_ids.filtered( lambda ml: ml.location_id._should_be_valued() and not ml. location_dest_id._should_be_valued() and not ml.owner_id) valued_quantity = 0 for valued_move_line in valued_move_lines: valued_quantity += valued_move_line.product_uom_id._compute_quantity( valued_move_line.qty_done, move.product_id.uom_id) # Find back incoming stock moves (called candidates here) to value this move. qty_to_take_on_candidates = quantity or valued_quantity candidates = move.product_id._get_fifo_candidates_in_move_with_company( move.company_id.id) new_standard_price = 0 tmp_value = 0 # to accumulate the value taken on the candidates for candidate in candidates: new_standard_price = candidate.price_unit if candidate.remaining_qty <= qty_to_take_on_candidates: qty_taken_on_candidate = candidate.remaining_qty else: qty_taken_on_candidate = qty_to_take_on_candidates # As applying a landed cost do not update the unit price, naivelly doing # something like qty_taken_on_candidate * candidate.price_unit won't make # the additional value brought by the landed cost go away. candidate_price_unit = candidate.remaining_value / candidate.remaining_qty value_taken_on_candidate = qty_taken_on_candidate * candidate_price_unit candidate_vals = { 'remaining_qty': candidate.remaining_qty - qty_taken_on_candidate, 'remaining_value': candidate.remaining_value - value_taken_on_candidate, } candidate.write(candidate_vals) qty_to_take_on_candidates -= qty_taken_on_candidate tmp_value += value_taken_on_candidate if qty_to_take_on_candidates == 0: break # Update the standard price with the price of the last used candidate, if any. if new_standard_price and move.product_id.cost_method == 'fifo': move.product_id.sudo().with_context(force_company=move.company_id.id) \ .standard_price = new_standard_price # If there's still quantity to value but we're out of candidates, we fall in the # negative stock use case. We chose to value the out move at the price of the # last out and a correction entry will be made once `_fifo_vacuum` is called. if qty_to_take_on_candidates == 0: move.write({ 'value': -tmp_value if not quantity else move.value or -tmp_value, # outgoing move are valued negatively 'price_unit': -tmp_value / (move.product_qty or quantity), }) elif qty_to_take_on_candidates > 0: last_fifo_price = new_standard_price or move.product_id.standard_price negative_stock_value = last_fifo_price * -qty_to_take_on_candidates tmp_value += abs(negative_stock_value) vals = { 'remaining_qty': move.remaining_qty + -qty_to_take_on_candidates, 'remaining_value': move.remaining_value + negative_stock_value, 'value': -tmp_value, 'price_unit': -1 * last_fifo_price, } move.write(vals) return tmp_value def _run_valuation(self, quantity=None): self.ensure_one() value_to_return = 0 if self._is_in(): valued_move_lines = self.move_line_ids.filtered( lambda ml: not ml.location_id._should_be_valued() and ml. location_dest_id._should_be_valued() and not ml.owner_id) valued_quantity = 0 for valued_move_line in valued_move_lines: valued_quantity += valued_move_line.product_uom_id._compute_quantity( valued_move_line.qty_done, self.product_id.uom_id) # Note: we always compute the fifo `remaining_value` and `remaining_qty` fields no # matter which cost method is set, to ease the switching of cost method. vals = {} price_unit = self._get_price_unit() value = price_unit * (quantity or valued_quantity) value_to_return = value if quantity is None or not self.value else self.value vals = { 'price_unit': price_unit, 'value': value_to_return, 'remaining_value': value if quantity is None else self.remaining_value + value, } vals[ 'remaining_qty'] = valued_quantity if quantity is None else self.remaining_qty + quantity if self.product_id.cost_method == 'standard': value = self.product_id.standard_price * (quantity or valued_quantity) value_to_return = value if quantity is None or not self.value else self.value vals.update({ 'price_unit': self.product_id.standard_price, 'value': value_to_return, }) self.write(vals) elif self._is_out(): valued_move_lines = self.move_line_ids.filtered( lambda ml: ml.location_id._should_be_valued() and not ml. location_dest_id._should_be_valued() and not ml.owner_id) valued_quantity = 0 for valued_move_line in valued_move_lines: valued_quantity += valued_move_line.product_uom_id._compute_quantity( valued_move_line.qty_done, self.product_id.uom_id) self.env['stock.move']._run_fifo(self, quantity=quantity) if self.product_id.cost_method in ['standard', 'average']: curr_rounding = self.company_id.currency_id.rounding value = -float_round( self.product_id.standard_price * (valued_quantity if quantity is None else quantity), precision_rounding=curr_rounding) value_to_return = value if quantity is None else self.value + value self.write({ 'value': value_to_return, 'price_unit': value / valued_quantity, }) elif self._is_dropshipped() or self._is_dropshipped_returned(): curr_rounding = self.company_id.currency_id.rounding if self.product_id.cost_method in ['fifo']: price_unit = self._get_price_unit() # see test_dropship_fifo_perpetual_anglosaxon_ordered self.product_id.standard_price = price_unit else: price_unit = self.product_id.standard_price value = float_round(self.product_qty * price_unit, precision_rounding=curr_rounding) value_to_return = value if self._is_dropshipped() else -value # In move have a positive value, out move have a negative value, let's arbitrary say # dropship are positive. self.write({ 'value': value_to_return, 'price_unit': price_unit if self._is_dropshipped() else -price_unit, }) return value_to_return def _action_done(self): self.product_price_update_before_done() res = super(StockMove, self)._action_done() for move in res: # Apply restrictions on the stock move to be able to make # consistent accounting entries. if move._is_in() and move._is_out(): raise UserError( _("The move lines are not in a consistent state: some are entering and other are leaving the company." )) company_src = move.mapped('move_line_ids.location_id.company_id') company_dst = move.mapped( 'move_line_ids.location_dest_id.company_id') try: if company_src: company_src.ensure_one() if company_dst: company_dst.ensure_one() except ValueError: raise UserError( _("The move lines are not in a consistent states: they do not share the same origin or destination company." )) if company_src and company_dst and company_src.id != company_dst.id: raise UserError( _("The move lines are not in a consistent states: they are doing an intercompany in a single step while they should go through the intercompany transit location." )) move._run_valuation() for move in res.filtered( lambda m: m.product_id.valuation == 'real_time' and (m._is_in() or m._is_out() or m._is_dropshipped() or m. _is_dropshipped_returned())): move._account_entry_move() return res @api.multi def product_price_update_before_done(self, forced_qty=None): tmpl_dict = defaultdict(lambda: 0.0) # adapt standard price on incomming moves if the product cost_method is 'average' std_price_update = {} for move in self.filtered(lambda move: move._is_in() and move. product_id.cost_method == 'average'): product_tot_qty_available = move.product_id.qty_available + tmpl_dict[ move.product_id.id] rounding = move.product_id.uom_id.rounding qty_done = move.product_uom._compute_quantity( move.quantity_done, move.product_id.uom_id) if float_is_zero(product_tot_qty_available, precision_rounding=rounding): new_std_price = move._get_price_unit() elif float_is_zero(product_tot_qty_available + move.product_qty, precision_rounding=rounding) or \ float_is_zero(product_tot_qty_available + qty_done, precision_rounding=rounding): new_std_price = move._get_price_unit() else: # Get the standard price amount_unit = std_price_update.get( (move.company_id.id, move.product_id.id)) or move.product_id.standard_price qty = forced_qty or qty_done new_std_price = ((amount_unit * product_tot_qty_available) + (move._get_price_unit() * qty)) / ( product_tot_qty_available + qty) tmpl_dict[move.product_id.id] += qty_done # Write the standard price, as SUPERUSER_ID because a warehouse manager may not have the right to write on products move.product_id.with_context( force_company=move.company_id.id).sudo().write( {'standard_price': new_std_price}) std_price_update[move.company_id.id, move.product_id.id] = new_std_price @api.model def _fifo_vacuum(self): """ Every moves that need to be fixed are identifiable by having a negative `remaining_qty`. """ for move in self.filtered( lambda m: (m._is_in() or m._is_out()) and m.remaining_qty < 0): domain = [('remaining_qty', '>', 0), '|', ('date', '>', move.date), '&', ('date', '=', move.date), ('id', '>', move.id)] domain += move._get_in_domain() candidates = self.search(domain, order='date, id') if not candidates: continue qty_to_take_on_candidates = abs(move.remaining_qty) qty_taken_on_candidates = 0 tmp_value = 0 for candidate in candidates: if candidate.remaining_qty <= qty_to_take_on_candidates: qty_taken_on_candidate = candidate.remaining_qty else: qty_taken_on_candidate = qty_to_take_on_candidates qty_taken_on_candidates += qty_taken_on_candidate value_taken_on_candidate = qty_taken_on_candidate * candidate.price_unit candidate_vals = { 'remaining_qty': candidate.remaining_qty - qty_taken_on_candidate, 'remaining_value': candidate.remaining_value - value_taken_on_candidate, } candidate.write(candidate_vals) qty_to_take_on_candidates -= qty_taken_on_candidate tmp_value += value_taken_on_candidate if qty_to_take_on_candidates == 0: break # When working with `price_unit`, beware that out move are negative. move_price_unit = move.price_unit if move._is_out( ) else -1 * move.price_unit # Get the estimated value we will correct. remaining_value_before_vacuum = qty_taken_on_candidates * move_price_unit new_remaining_qty = move.remaining_qty + qty_taken_on_candidates new_remaining_value = new_remaining_qty * abs(move.price_unit) corrected_value = remaining_value_before_vacuum + tmp_value move.write({ 'remaining_value': new_remaining_value, 'remaining_qty': new_remaining_qty, 'value': move.value - corrected_value, }) if move.product_id.valuation == 'real_time': # If `corrected_value` is 0, absolutely do *not* call `_account_entry_move`. We # force the amount in the context, but in the case it is 0 it'll create an entry # for the entire cost of the move. This case happens when the candidates moves # entirely compensate the problematic move. if move.company_id.currency_id.is_zero(corrected_value): continue if move._is_in(): # If we just compensated an IN move that has a negative remaining # quantity, it means the move has returned more items than it received. # The correction should behave as a return too. As `_account_entry_move` # will post the natural values for an IN move (credit IN account, debit # OUT one), we inverse the sign to create the correct entries. move.with_context(force_valuation_amount=-corrected_value, forced_quantity=0)._account_entry_move() else: move.with_context(force_valuation_amount=corrected_value, forced_quantity=0)._account_entry_move() @api.model def _run_fifo_vacuum(self): # Call `_fifo_vacuum` on concerned moves fifo_valued_products = self.env['product.product'] fifo_valued_products |= self.env['product.template'].search([ ('property_cost_method', '=', 'fifo') ]).mapped('product_variant_ids') fifo_valued_categories = self.env['product.category'].search([ ('property_cost_method', '=', 'fifo') ]) fifo_valued_products |= self.env['product.product'].search([ ('categ_id', 'child_of', fifo_valued_categories.ids) ]) moves_to_vacuum = self.search([('product_id', 'in', fifo_valued_products.ids), ('remaining_qty', '<', 0)] + self._get_all_base_domain()) moves_to_vacuum._fifo_vacuum() @api.multi def _get_accounting_data_for_valuation(self): """ Return the accounts and journal to use to post Journal Entries for the real-time valuation of the quant. """ self.ensure_one() accounts_data = self.product_id.product_tmpl_id.get_product_accounts() if self.location_id.valuation_out_account_id: acc_src = self.location_id.valuation_out_account_id.id else: acc_src = accounts_data['stock_input'].id if self.location_dest_id.valuation_in_account_id: acc_dest = self.location_dest_id.valuation_in_account_id.id else: acc_dest = accounts_data['stock_output'].id acc_valuation = accounts_data.get('stock_valuation', False) if acc_valuation: acc_valuation = acc_valuation.id if not accounts_data.get('stock_journal', False): raise UserError( _('You don\'t have any stock journal defined on your product category, check if you have installed a chart of accounts.' )) if not acc_src: raise UserError( _('Cannot find a stock input account for the product %s. You must define one on the product category, or on the location, before processing this operation.' ) % (self.product_id.display_name)) if not acc_dest: raise UserError( _('Cannot find a stock output account for the product %s. You must define one on the product category, or on the location, before processing this operation.' ) % (self.product_id.display_name)) if not acc_valuation: raise UserError( _('You don\'t have any stock valuation account defined on your product category. You must define one before processing this operation.' )) journal_id = accounts_data['stock_journal'].id return journal_id, acc_src, acc_dest, acc_valuation def _prepare_account_move_line(self, qty, cost, credit_account_id, debit_account_id): """ Generate the account.move.line values to post to track the stock valuation difference due to the processing of the given quant. """ self.ensure_one() if self._context.get('force_valuation_amount'): valuation_amount = self._context.get('force_valuation_amount') else: valuation_amount = cost # the standard_price of the product may be in another decimal precision, or not compatible with the coinage of # the company currency... so we need to use round() before creating the accounting entries. debit_value = self.company_id.currency_id.round(valuation_amount) # check that all data is correct if self.company_id.currency_id.is_zero( debit_value) and not self.env['ir.config_parameter'].sudo( ).get_param('stock_account.allow_zero_cost'): raise UserError( _("The cost of %s is currently equal to 0. Change the cost or the configuration of your product to avoid an incorrect valuation." ) % (self.product_id.display_name, )) credit_value = debit_value valuation_partner_id = self._get_partner_id_for_valuation_lines() res = [(0, 0, line_vals) for line_vals in self._generate_valuation_lines_data( valuation_partner_id, qty, debit_value, credit_value, debit_account_id, credit_account_id).values()] return res def _generate_valuation_lines_data(self, partner_id, qty, debit_value, credit_value, debit_account_id, credit_account_id): # This method returns a dictonary to provide an easy extension hook to modify the valuation lines (see purchase for an example) self.ensure_one() if self._context.get('forced_ref'): ref = self._context['forced_ref'] else: ref = self.picking_id.name debit_line_vals = { 'name': self.name, 'product_id': self.product_id.id, 'quantity': qty, 'product_uom_id': self.product_id.uom_id.id, 'ref': ref, 'partner_id': partner_id, 'debit': debit_value if debit_value > 0 else 0, 'credit': -debit_value if debit_value < 0 else 0, 'account_id': debit_account_id, } credit_line_vals = { 'name': self.name, 'product_id': self.product_id.id, 'quantity': qty, 'product_uom_id': self.product_id.uom_id.id, 'ref': ref, 'partner_id': partner_id, 'credit': credit_value if credit_value > 0 else 0, 'debit': -credit_value if credit_value < 0 else 0, 'account_id': credit_account_id, } rslt = { 'credit_line_vals': credit_line_vals, 'debit_line_vals': debit_line_vals } if credit_value != debit_value: # for supplier returns of product in average costing method, in anglo saxon mode diff_amount = debit_value - credit_value price_diff_account = self.product_id.property_account_creditor_price_difference if not price_diff_account: price_diff_account = self.product_id.categ_id.property_account_creditor_price_difference_categ if not price_diff_account: raise UserError( _('Configuration error. Please configure the price difference account on the product or its category to process this operation.' )) rslt['price_diff_line_vals'] = { 'name': self.name, 'product_id': self.product_id.id, 'quantity': qty, 'product_uom_id': self.product_id.uom_id.id, 'ref': ref, 'partner_id': partner_id, 'credit': diff_amount > 0 and diff_amount or 0, 'debit': diff_amount < 0 and -diff_amount or 0, 'account_id': price_diff_account.id, } return rslt def _get_partner_id_for_valuation_lines(self): return (self.picking_id.partner_id and self.env['res.partner']._find_accounting_partner( self.picking_id.partner_id).id) or False def _prepare_move_split_vals(self, uom_qty): vals = super(StockMove, self)._prepare_move_split_vals(uom_qty) vals['to_refund'] = self.to_refund return vals def _create_account_move_line(self, credit_account_id, debit_account_id, journal_id): self.ensure_one() AccountMove = self.env['account.move'] quantity = self.env.context.get('forced_quantity', self.product_qty) quantity = quantity if self._is_in() else -1 * quantity # Make an informative `ref` on the created account move to differentiate between classic # movements, vacuum and edition of past moves. ref = self.picking_id.name if self.env.context.get('force_valuation_amount'): if self.env.context.get('forced_quantity') == 0: ref = 'Revaluation of %s (negative inventory)' % ref elif self.env.context.get('forced_quantity') is not None: ref = 'Correction of %s (modification of past move)' % ref move_lines = self.with_context( forced_ref=ref)._prepare_account_move_line(quantity, abs(self.value), credit_account_id, debit_account_id) if move_lines: date = self._context.get('force_period_date', fields.Date.context_today(self)) new_account_move = AccountMove.sudo().create({ 'journal_id': journal_id, 'line_ids': move_lines, 'date': date, 'ref': ref, 'stock_move_id': self.id, }) new_account_move.post() def _account_entry_move(self): """ Accounting Valuation Entries """ self.ensure_one() if self.product_id.type != 'product': # no stock valuation for consumable products return False if self.restrict_partner_id: # if the move isn't owned by the company, we don't make any valuation return False location_from = self.location_id location_to = self.location_dest_id company_from = self._is_out() and self.mapped( 'move_line_ids.location_id.company_id') or False company_to = self._is_in() and self.mapped( 'move_line_ids.location_dest_id.company_id') or False # Create Journal Entry for products arriving in the company; in case of routes making the link between several # warehouse of the same company, the transit location belongs to this company, so we don't need to create accounting entries if self._is_in(): journal_id, acc_src, acc_dest, acc_valuation = self._get_accounting_data_for_valuation( ) if location_from and location_from.usage == 'customer': # goods returned from customer self.with_context( force_company=company_to.id)._create_account_move_line( acc_dest, acc_valuation, journal_id) else: self.with_context( force_company=company_to.id)._create_account_move_line( acc_src, acc_valuation, journal_id) # Create Journal Entry for products leaving the company if self._is_out(): journal_id, acc_src, acc_dest, acc_valuation = self._get_accounting_data_for_valuation( ) if location_to and location_to.usage == 'supplier': # goods returned to supplier self.with_context( force_company=company_from.id)._create_account_move_line( acc_valuation, acc_src, journal_id) else: self.with_context( force_company=company_from.id)._create_account_move_line( acc_valuation, acc_dest, journal_id) if self.company_id.anglo_saxon_accounting: # Creates an account entry from stock_input to stock_output on a dropship move. https://github.com/eagle/eagle/issues/12687 journal_id, acc_src, acc_dest, acc_valuation = self._get_accounting_data_for_valuation( ) if self._is_dropshipped(): self.with_context(force_company=self.company_id.id )._create_account_move_line( acc_src, acc_dest, journal_id) elif self._is_dropshipped_returned(): self.with_context(force_company=self.company_id.id )._create_account_move_line( acc_dest, acc_src, journal_id) if self.company_id.anglo_saxon_accounting: #eventually reconcile together the invoice and valuation accounting entries on the stock interim accounts self._get_related_invoices()._anglo_saxon_reconcile_valuation( product=self.product_id) def _get_related_invoices( self): # To be overridden in purchase and sale_stock """ This method is overrided in both purchase and sale_stock modules to adapt to the way they mix stock moves with invoices. """ return self.env['account.invoice']
class FleetVehicle(models.Model): _inherit = 'fleet.vehicle' co2_fee = fields.Float(compute='_compute_co2_fee', string="CO2 Fee", store=True) total_depreciated_cost = fields.Float( compute='_compute_total_depreciated_cost', store=True, string="Total Cost (Depreciated)", track_visibility="onchange", help="This includes all the depreciated costs and the CO2 fee") total_cost = fields.Float( compute='_compute_total_cost', string="Total Cost", help="This include all the costs and the CO2 fee") fuel_type = fields.Selection(required=True, default='diesel') atn = fields.Float(compute='_compute_car_atn', string="ATN") acquisition_date = fields.Date(required=True) @api.depends('co2_fee', 'log_contracts', 'log_contracts.state', 'log_contracts.recurring_cost_amount_depreciated') def _compute_total_depreciated_cost(self): for car in self: car.total_depreciated_cost = car.co2_fee + \ sum(car.log_contracts.filtered( lambda contract: contract.state == 'open' ).mapped('recurring_cost_amount_depreciated')) @api.depends('co2_fee', 'log_contracts', 'log_contracts.state', 'log_contracts.cost_generated') def _compute_total_cost(self): for car in self: car.total_cost = car.co2_fee contracts = car.log_contracts.filtered( lambda contract: contract.state == 'open' and contract. cost_frequency != 'no') for contract in contracts: if contract.cost_frequency == "daily": car.total_cost += contract.cost_generated * 30.0 elif contract.cost_frequency == "weekly": car.total_cost += contract.cost_generated * 4.0 elif contract.cost_frequency == "monthly": car.total_cost += contract.cost_generated elif contract.cost_frequency == "yearly": car.total_cost += contract.cost_generated / 12.0 def _get_co2_fee(self, co2, fuel_type): fuel_coefficient = { 'diesel': 600, 'gasoline': 768, 'lpg': 990, 'electric': 0, 'hybrid': 600 } co2_fee = 0 if fuel_type and fuel_type != 'electric': if not co2: co2 = 165 if fuel_type in ['diesel', 'hybrid'] else 182 co2_fee = (((co2 * 9.0) - fuel_coefficient.get(fuel_type, 0)) * 144.97 / 114.08) / 12.0 return max(co2_fee, 26.47) @api.depends('co2', 'fuel_type') def _compute_co2_fee(self): for car in self: car.co2_fee = self._get_co2_fee(car.co2, car.fuel_type) @api.depends('fuel_type', 'car_value', 'acquisition_date', 'co2') def _compute_car_atn(self): for car in self: car.atn = car._get_car_atn(car.acquisition_date, car.car_value, car.fuel_type, car.co2) @api.depends('model_id', 'license_plate', 'log_contracts', 'acquisition_date', 'co2_fee', 'log_contracts', 'log_contracts.state', 'log_contracts.recurring_cost_amount_depreciated') def _compute_vehicle_name(self): super(FleetVehicle, self)._compute_vehicle_name() for vehicle in self: acquisition_date = vehicle._get_acquisition_date() vehicle.name += u" \u2022 " + str( round(vehicle.total_depreciated_cost, 2)) + u" \u2022 " + acquisition_date @api.model def create(self, vals): res = super(FleetVehicle, self).create(vals) if not res.log_contracts: self.env['fleet.vehicle.log.contract'].create({ 'vehicle_id': res.id, 'recurring_cost_amount_depreciated': res.model_id.default_recurring_cost_amount_depreciated, 'purchaser_id': res.driver_id.id, }) return res def _get_acquisition_date(self): self.ensure_one() return babel.dates.format_date(date=self.acquisition_date, format='MMMM y', locale=self._context.get('lang') or 'en_US') def _get_car_atn(self, acquisition_date, car_value, fuel_type, co2): # Compute the correction coefficient from the age of the car now = Date.today() if acquisition_date: number_of_month = ((now.year - acquisition_date.year) * 12.0 + now.month - acquisition_date.month + int(bool(now.day - acquisition_date.day + 1))) if number_of_month <= 12: age_coefficient = 1.00 elif number_of_month <= 24: age_coefficient = 0.94 elif number_of_month <= 36: age_coefficient = 0.88 elif number_of_month <= 48: age_coefficient = 0.82 elif number_of_month <= 60: age_coefficient = 0.76 else: age_coefficient = 0.70 car_value = car_value * age_coefficient # Compute atn value from corrected car_value magic_coeff = 6.0 / 7.0 # Don't ask me why if fuel_type == 'electric': atn = 0.0 else: if fuel_type in ['diesel', 'hybrid']: reference = 88.0 else: reference = 107.0 if not co2: co2 = 195 if fuel_type in ['diesel', 'hybrid'] else 205 if co2 <= reference: atn = car_value * max(0.04, (0.055 - 0.001 * (reference - co2))) * magic_coeff else: atn = car_value * min(0.18, (0.055 + 0.001 * (co2 - reference))) * magic_coeff return max(1310, atn) / 12.0 @api.onchange('model_id') def _onchange_model_id(self): self.car_value = self.model_id.default_car_value self.co2 = self.model_id.default_co2 self.fuel_type = self.model_id.default_fuel_type
class OpAssignment(models.Model): _name = "op.assignment" _inherit = "mail.thread" _description = "Assignment" _order = "submission_date DESC" name = fields.Char('Name', size=64, required=True) course_id = fields.Many2one('op.course', 'Course', required=True) batch_id = fields.Many2one('op.batch', 'Batch', required=True) subject_id = fields.Many2one('op.subject', 'Subject', required=True) faculty_id = fields.Many2one('op.faculty', 'Faculty', default=lambda self: self.env['op.faculty']. search([('user_id', '=', self.env.uid)]), required=True) assignment_type_id = fields.Many2one('op.assignment.type', 'Assignment Type', required=True) marks = fields.Float('Marks', required=True, track_visibility='onchange') description = fields.Text('Description', required=True) state = fields.Selection([ ('draft', 'Draft'), ('publish', 'Published'), ('finish', 'Finished'), ('cancel', 'Cancel'), ], 'State', required=True, default='draft', track_visibility='onchange') issued_date = fields.Datetime(string='Issued Date', required=True, default=lambda self: fields.Datetime.now()) submission_date = fields.Datetime('Submission Date', required=True, track_visibility='onchange') allocation_ids = fields.Many2many('op.student', string='Allocated To') assignment_sub_line = fields.One2many('op.assignment.sub.line', 'assignment_id', 'Submissions') reviewer = fields.Many2one('op.faculty', 'Reviewer') @api.constrains('issued_date', 'submission_date') def check_dates(self): for record in self: issued_date = fields.Date.from_string(record.issued_date) submission_date = fields.Date.from_string(record.submission_date) if issued_date > submission_date: raise ValidationError( _("Submission Date cannot be set before Issue Date.")) @api.onchange('course_id') def onchange_course(self): self.batch_id = False if self.course_id: subject_ids = self.env['op.course'].search([ ('id', '=', self.course_id.id) ]).subject_ids return {'domain': {'subject_id': [('id', 'in', subject_ids.ids)]}} def act_publish(self): result = self.state = 'publish' return result and result or False def act_finish(self): result = self.state = 'finish' return result and result or False def act_cancel(self): self.state = 'cancel' def act_set_to_draft(self): self.state = 'draft'
class SaleOrderTemplateLine(models.Model): _name = "sale.order.template.line" _description = "Quotation Template Line" _order = 'sale_order_template_id, sequence, id' sequence = fields.Integer( 'Sequence', help= "Gives the sequence order when displaying a list of sale quote lines.", default=10) sale_order_template_id = fields.Many2one('sale.order.template', 'Quotation Template Reference', required=True, ondelete='cascade', index=True) name = fields.Text('Description', required=True, translate=True) product_id = fields.Many2one('product.product', 'Product', domain=[('sale_ok', '=', True)]) price_unit = fields.Float('Unit Price', required=True, digits=dp.get_precision('Product Price')) discount = fields.Float('Discount (%)', digits=dp.get_precision('Discount'), default=0.0) product_uom_qty = fields.Float('Quantity', required=True, digits=dp.get_precision('Product UoS'), default=1) product_uom_id = fields.Many2one('uom.uom', 'Unit of Measure') display_type = fields.Selection([('line_section', "Section"), ('line_note', "Note")], default=False, help="Technical field for UX purpose.") @api.onchange('product_id') def _onchange_product_id(self): self.ensure_one() if self.product_id: name = self.product_id.name_get()[0][1] if self.product_id.description_sale: name += '\n' + self.product_id.description_sale self.name = name self.price_unit = self.product_id.lst_price self.product_uom_id = self.product_id.uom_id.id domain = { 'product_uom_id': [('category_id', '=', self.product_id.uom_id.category_id.id)] } return {'domain': domain} @api.onchange('product_uom_id') def _onchange_product_uom(self): if self.product_id and self.product_uom_id: self.price_unit = self.product_id.uom_id._compute_price( self.product_id.lst_price, self.product_uom_id) @api.model def create(self, values): if values.get('display_type', self.default_get(['display_type'])['display_type']): values.update(product_id=False, price_unit=0, product_uom_qty=0, product_uom_id=False) return super(SaleOrderTemplateLine, self).create(values) @api.multi def write(self, values): if 'display_type' in values and self.filtered( lambda line: line.display_type != values.get('display_type')): raise UserError( "You cannot change the type of a sale quote line. Instead you should delete the current line and create a new line of the proper type." ) return super(SaleOrderTemplateLine, self).write(values) _sql_constraints = [ ('accountable_product_id_required', "CHECK(display_type IS NOT NULL OR (product_id IS NOT NULL AND product_uom_id IS NOT NULL))", "Missing required product and UoM on accountable sale quote line."), ('non_accountable_fields_null', "CHECK(display_type IS NULL OR (product_id IS NULL AND price_unit = 0 AND product_uom_qty = 0 AND product_uom_id IS NULL))", "Forbidden product, unit price, quantity, and UoM on non-accountable sale quote line" ), ]