class Partner(models.Model): _inherit = "res.partner" def _get_l10n_do_dgii_payer_types_selection(self): """Return the list of payer types needed in invoices to clasify accordingly to DGII requirements.""" return [ ("taxpayer", _("Fiscal Tax Payer")), ("non_payer", _("Non Tax Payer")), ("nonprofit", _("Nonprofit Organization")), ("special", _("special from Tax Paying")), ("governmental", _("Governmental")), ("foreigner", _("Foreigner")), ] def _get_l10n_do_expense_type(self): """Return the list of expenses needed in invoices to clasify accordingly to DGII requirements.""" return [ ("01", _("01 - Personal")), ("02", _("02 - Work, Supplies and Services")), ("03", _("03 - Leasing")), ("04", _("04 - Fixed Assets")), ("05", _("05 - Representation")), ("06", _("06 - Admitted Deductions")), ("07", _("07 - Financial Expenses")), ("08", _("08 - Extraordinary Expenses")), ("09", _("09 - Cost & Expenses part of Sales")), ("10", _("10 - Assets Acquisitions")), ("11", _("11 - Insurance Expenses")), ] l10n_do_dgii_tax_payer_type = fields.Selection( selection="_get_l10n_do_dgii_payer_types_selection", compute="_compute_l10n_do_dgii_payer_type", inverse="_inverse_l10n_do_dgii_tax_payer_type", string="Taxpayer Type", index=True, store=True, ) l10n_do_expense_type = fields.Selection( selection="_get_l10n_do_expense_type", string="Cost & Expense Type", store=True, ) country_id = fields.Many2one( default=lambda self: self.env.ref("base.do") if self.env.user.company_id.country_id == self.env.ref("base.do") else False ) def _check_l10n_do_fiscal_fields(self, vals): if not self or self.parent_id: # Do not perform any check because child contacts # have readonly fiscal field. This also allows set # contacts parent, even if this changes any of its # fiscal fields. return fiscal_fields = [ field for field in ["name", "vat", "country_id"] # l10n_do_dgii_tax_payer_type ? if field in vals ] if ( fiscal_fields and not self.env.user.has_group( "l10n_do_accounting.group_l10n_do_edit_fiscal_partner" ) and self.env["account.move"] .sudo() .search( [ ("l10n_latam_use_documents", "=", True), ("country_code", "=", "DO"), ("commercial_partner_id", "=", self.id), ("state", "=", "posted"), ], limit=1, ) ): raise AccessError( _( "You are not allowed to modify %s after partner " "fiscal document issuing" ) % (", ".join(self._fields[f].string for f in fiscal_fields)) ) def write(self, vals): res = super(Partner, self).write(vals) self._check_l10n_do_fiscal_fields(vals) return res @api.depends("vat", "country_id", "name") def _compute_l10n_do_dgii_payer_type(self): """ Compute the type of partner depending on soft decisions""" company_id = self.env["res.company"].search( [("id", "=", self.env.user.company_id.id)] ) for partner in self: vat = str(partner.vat if partner.vat else partner.name) is_dominican_partner = bool(partner.country_id == self.env.ref("base.do")) if partner.country_id and not is_dominican_partner: partner.l10n_do_dgii_tax_payer_type = "foreigner" elif vat and ( not partner.l10n_do_dgii_tax_payer_type or partner.l10n_do_dgii_tax_payer_type == "non_payer" ): if partner.country_id and is_dominican_partner: if vat.isdigit() and len(vat) == 9: if not partner.vat: partner.vat = vat if partner.name and "MINISTERIO" in partner.name: partner.l10n_do_dgii_tax_payer_type = "governmental" elif partner.name and any( [n for n in ("IGLESIA", "ZONA FRANCA") if n in partner.name] ): partner.l10n_do_dgii_tax_payer_type = "special" elif vat.startswith("1"): partner.l10n_do_dgii_tax_payer_type = "taxpayer" elif vat.startswith("4"): partner.l10n_do_dgii_tax_payer_type = "nonprofit" else: partner.l10n_do_dgii_tax_payer_type = "taxpayer" elif len(vat) == 11: if vat.isdigit(): if not partner.vat: partner.vat = vat payer_type = ( "taxpayer" if company_id.l10n_do_default_client == "fiscal" else "non_payer" ) partner.l10n_do_dgii_tax_payer_type = payer_type else: partner.l10n_do_dgii_tax_payer_type = "non_payer" else: partner.l10n_do_dgii_tax_payer_type = "non_payer" elif not partner.l10n_do_dgii_tax_payer_type: partner.l10n_do_dgii_tax_payer_type = "non_payer" else: partner.l10n_do_dgii_tax_payer_type = ( partner.l10n_do_dgii_tax_payer_type ) def _inverse_l10n_do_dgii_tax_payer_type(self): for partner in self: partner.l10n_do_dgii_tax_payer_type = partner.l10n_do_dgii_tax_payer_type
class BaseModuleRecord(models.TransientModel): _name = 'base.module.record' _description = "Base Module Record" @api.model def _get_default_objects(self): names = ('ir.ui.view', 'ir.ui.menu', 'ir.model', 'ir.model.fields', 'ir.model.access', 'res.partner', 'res.partner.address', 'res.partner.category', 'workflow', 'workflow.activity', 'workflow.transition', 'ir.actions.server', 'ir.server.object.lines') return self.env['ir.model'].search([('model', 'in', names)]) check_date = fields.Datetime('Record from Date', required=True, default=fields.Datetime.now) objects = fields.Many2many('ir.model', 'base_module_record_object_rel', 'objects', 'model_id', 'Objects', default=_get_default_objects) filter_cond = fields.Selection( [('created', 'Created'), ('modified', 'Modified'), ('created_modified', 'Created & Modified')], 'Records only', required=True, default='created') @api.multi def record_objects(self): data = self.read([])[0] check_date = data['check_date'] filter_cond = data['filter_cond'] mod_obj = self.env['ir.model'] recording_data = [] for obj_id in data['objects']: obj_name = (mod_obj.browse(obj_id)).model obj_pool = self.env[obj_name] if filter_cond == 'created': search_condition = [('create_date', '>', check_date)] elif filter_cond == 'modified': search_condition = [('write_date', '>', check_date)] elif filter_cond == 'created_modified': search_condition = [ '|', ('create_date', '>', check_date), ('write_date', '>', check_date) ] if '_log_access' in dir(obj_pool): if not (obj_pool._log_access): search_condition = [] if '_auto' in dir(obj_pool): if not obj_pool._auto: continue search_ids = obj_pool.search(search_condition) for s_id in search_ids: dbname = self.env.cr.dbname args = (dbname, self.env.user.id, obj_name, 'copy', s_id.id, {}) recording_data.append(('query', args, {}, s_id.id)) if len(recording_data): res_id = self.env.ref('base_module_record.info_start_form_view').id self = self.with_context({'recording_data': recording_data}) return { 'name': _('Module Recording'), 'context': self._context, 'view_type': 'form', 'view_mode': 'form', 'res_model': 'base.module.record.objects', 'views': [(res_id, 'form')], 'type': 'ir.actions.act_window', 'target': 'new', } res_id = self.env.ref( 'base_module_record.module_recording_message_view').id return { 'name': _('Module Recording'), 'context': self._context, 'view_type': 'form', 'view_mode': 'form', 'res_model': 'base.module.record.objects', 'views': [(res_id, 'form')], 'type': 'ir.actions.act_window', 'target': 'new', }
class SaleInvoicePlan(models.Model): _name = "sale.invoice.plan" _description = "Invoice Planning Detail" _order = "installment" sale_id = fields.Many2one( comodel_name="sale.order", string="Sales Order", index=True, readonly=True, ondelete="cascade", ) partner_id = fields.Many2one( comodel_name="res.partner", string="Customer", related="sale_id.partner_id", store=True, index=True, ) state = fields.Selection( [ ("draft", "Quotation"), ("sent", "Quotation Sent"), ("sale", "Sales Order"), ("done", "Locked"), ("cancel", "Cancelled"), ], string="Status", related="sale_id.state", store=True, index=True, ) installment = fields.Integer(string="Installment") plan_date = fields.Date(string="Plan Date", required=True) invoice_type = fields.Selection( [("advance", "Advance"), ("installment", "Installment")], string="Type", required=True, default="installment", ) last = fields.Boolean( string="Last Installment", compute="_compute_last", help="Last installment will create invoice use remaining amount", ) percent = fields.Float( string="Percent", digits="Product Unit of Measure", help="This percent will be used to calculate new quantity", ) invoice_move_ids = fields.Many2many( "account.move", relation="sale_invoice_plan_invoice_rel", column1="plan_id", column2="move_id", string="Invoices", readonly=True, ) to_invoice = fields.Boolean( string="Next Invoice", compute="_compute_to_invoice", help="If this line is ready to create new invoice", ) invoiced = fields.Boolean( string="Invoice Created", compute="_compute_invoiced", help="If this line already invoiced", ) _sql_constraint = [( "unique_instalment", "UNIQUE (sale_id, installment)", "Installment must be unique on invoice plan", )] def _compute_to_invoice(self): """ If any invoice is in draft/open/paid do not allow to create inv. Only if previous to_invoice is False, it is eligible to_invoice. """ for rec in self: rec.to_invoice = False for rec in self.sorted("installment"): if rec.state != "sale": # Not confirmed, no to_invoice continue if not rec.invoiced: rec.to_invoice = True break def _compute_invoiced(self): for rec in self: invoiced = rec.invoice_move_ids.filtered(lambda l: l.state in ("draft", "posted")) rec.invoiced = invoiced and True or False def _compute_last(self): for rec in self: last = max(rec.sale_id.invoice_plan_ids.mapped("installment")) rec.last = rec.installment == last def _compute_new_invoice_quantity(self, invoice_move): self.ensure_one() if self.last: # For last install, let the system do the calc. return percent = self.percent move = invoice_move.with_context({"check_move_validity": False}) for line in move.invoice_line_ids: assert (len(line.sale_line_ids) >= 0), "No matched order line for invoice line" order_line = fields.first(line.sale_line_ids) if order_line.is_downpayment: # based on 1 unit line.write({"quantity": -percent / 100}) else: plan_qty = order_line.product_uom_qty * (percent / 100) prec = order_line.product_uom.rounding if float_compare(plan_qty, line.quantity, prec) == 1: raise ValidationError( _("Plan quantity: %s, exceed invoiceable quantity: %s" "\nProduct should be delivered before invoice") % (plan_qty, line.quantity)) line.write({"quantity": plan_qty}) # Call this method to recompute dr/cr lines move._move_autocomplete_invoice_lines_values()
class HrPayrollReport(models.Model): _name = "hr.payroll.report" _description = "Payroll Analysis Report" _auto = False _rec_name = 'date_from' _order = 'date_from desc' count = fields.Integer('# Payslip', group_operator="sum", readonly=True) count_work = fields.Integer('Work Days', group_operator="sum", readonly=True) count_work_hours = fields.Integer('Work Hours', group_operator="sum", readonly=True) count_leave = fields.Integer('Days of Paid Time Off', group_operator="sum", readonly=True) count_leave_unpaid = fields.Integer('Days of Unpaid Time Off', group_operator="sum", readonly=True) count_unforeseen_absence = fields.Integer('Days of Unforeseen Absence', group_operator="sum", readonly=True) name = fields.Char('Payslip Name', readonly=True) date_from = fields.Date('Start Date', readonly=True) date_to = fields.Date('End Date', readonly=True) company_id = fields.Many2one('res.company', 'Company', readonly=True) employee_id = fields.Many2one('hr.employee', 'Employee', readonly=True) department_id = fields.Many2one('hr.department', 'Department', readonly=True) job_id = fields.Many2one('hr.job', 'Job Position', readonly=True) number_of_days = fields.Float('Number of Days', readonly=True) number_of_hours = fields.Float('Number of Hours', readonly=True) net_wage = fields.Float('Net Wage', readonly=True) basic_wage = fields.Float('Basic Wage', readonly=True) gross_wage = fields.Float('Gross Wage', readonly=True) leave_basic_wage = fields.Float('Basic Wage for Time Off', readonly=True) work_code = fields.Many2one('hr.work.entry.type', 'Work type', readonly=True) work_type = fields.Selection([('1', 'Regular Working Day'), ('2', 'Paid Time Off'), ('3', 'Unpaid Time Off')], string='Work, (un)paid Time Off', readonly=True) def init(self): query = """ SELECT p.id as id, CASE WHEN wd.id = min_id.min_line THEN 1 ELSE 0 END as count, CASE WHEN wet.is_leave THEN 0 ELSE wd.number_of_days END as count_work, CASE WHEN wet.is_leave THEN 0 ELSE wd.number_of_hours END as count_work_hours, CASE WHEN wet.is_leave and wd.amount <> 0 THEN wd.number_of_days ELSE 0 END as count_leave, CASE WHEN wet.is_leave and wd.amount = 0 THEN wd.number_of_days ELSE 0 END as count_leave_unpaid, CASE WHEN wet.is_unforeseen THEN wd.number_of_days ELSE 0 END as count_unforeseen_absence, CASE WHEN wet.is_leave THEN wd.amount ELSE 0 END as leave_basic_wage, p.name as name, p.date_from as date_from, p.date_to as date_to, e.id as employee_id, e.department_id as department_id, c.job_id as job_id, e.company_id as company_id, wet.id as work_code, CASE WHEN wet.is_leave IS NOT TRUE THEN '1' WHEN wd.amount = 0 THEN '3' ELSE '2' END as work_type, wd.number_of_days as number_of_days, wd.number_of_hours as number_of_hours, CASE WHEN wd.id = min_id.min_line THEN pln.total ELSE 0 END as net_wage, CASE WHEN wd.id = min_id.min_line THEN plb.total ELSE 0 END as basic_wage, CASE WHEN wd.id = min_id.min_line THEN plg.total ELSE 0 END as gross_wage FROM (SELECT * FROM hr_payslip WHERE state IN ('done', 'paid')) p left join hr_employee e on (p.employee_id = e.id) left join hr_payslip_worked_days wd on (wd.payslip_id = p.id) left join hr_work_entry_type wet on (wet.id = wd.work_entry_type_id) left join (select payslip_id, min(id) as min_line from hr_payslip_worked_days group by payslip_id) min_id on (min_id.payslip_id = p.id) left join hr_payslip_line pln on (pln.slip_id = p.id and pln.code = 'NET') left join hr_payslip_line plb on (plb.slip_id = p.id and plb.code = 'BASIC') left join hr_payslip_line plg on (plg.slip_id = p.id and plg.code = 'GROSS') left join hr_contract c on (p.contract_id = c.id) GROUP BY e.id, e.department_id, e.company_id, wd.id, wet.id, p.id, p.name, p.date_from, p.date_to, pln.total, plb.total, plg.total, min_id.min_line, c.id""" tools.drop_view_if_exists(self.env.cr, self._table) self.env.cr.execute( sql.SQL("CREATE or REPLACE VIEW {} as ({})").format( sql.Identifier(self._table), sql.SQL(query)))
class deposito_serivice_products(models.Model): _name = "deposito.service.products" _description = "Servicios Deposito" _order = "id ASC" name = fields.Char() deposito_srv_id = fields.Many2one(comodel_name='deposito.service.base', string='Carpeta Relacionada') state = fields.Selection( [ ('draft', 'Borrador'), ('confirm', 'Confirmado'), ('inprocess', 'En proceso'), ('cancel', 'Cancelado'), ('done', 'Realizado'), ], string='Status', index=True, readonly=True, default='draft', track_visibility='onchange', copy=False, ) product_type = fields.Selection([('propio', 'Propio'), ('terceros', 'Terceros')], string='Origen del Servicio') partner_invoice_id = fields.Many2one(comodel_name='res.partner', string='Cliente') pricelist_id = fields.Many2one('product.pricelist.item', string='Tarifa') product_id = fields.Many2one(comodel_name='product.product', string='Servicio', domain=[('product_tmpl_id.type', '=', 'service'), ('sale_ok', '=', True)], required=False, change_default=True, ondelete='restrict') supplier_id = fields.Many2one(comodel_name='res.partner', string='Proveedor', domain=[('supplier', '=', True)]) origin_id = fields.Many2one(comodel_name='res.partner.address.ext', string='Origen') destiny_id = fields.Many2one(comodel_name='res.partner.address.ext', string='Destino') matricula = fields.Char(string=u'Matricula') matricula_dos_id = fields.Many2one(comodel_name='fleet.vehicle', string=u'Matrícula dos') vehicle_id = fields.Many2one(comodel_name='fleet.vehicle', string=u'Matrícula', domain=[('is_ras_property', '=', True)]) vehicle_type = fields.Selection(related='vehicle_id.vehicle_type', type='char', readonly=True) driver_id = fields.Many2one('hr.employee', 'Chofer', help=u'Chofer del Vehículo') chofer = fields.Char('Chofer') matricula_dos_fletero = fields.Char(string='Matricula Dos Fletero') matricula_fletero = fields.Many2one(comodel_name='fleet.vehicle', string='Matricula Fletero', domain=[('is_ras_property', '=', False) ]) currency_id = fields.Many2one(comodel_name='res.currency', string='Moneda') importe = fields.Float(string='Importe') valor_compra_currency_id = fields.Many2one(comodel_name='res.currency', string='Moneda Compra') valor_compra = fields.Monetary(string='Valor Compra', currency_field='valor_compra_currency_id') currency_id_chofer = fields.Many2one(comodel_name='res.currency', string='Moneda Comisión Chofer') driver_commission = fields.Float('Comisión de chofer') is_invoiced = fields.Boolean( 'Facturable', help='Marque esta casilla si este servicio no se factura', default=True) is_outgoing = fields.Boolean( '¿Es Gasto?', help='Marque esta casilla si este servicio es un Gasto', default=True) invoiced = fields.Boolean(string='¿Facturado?', copy=False) oc = fields.Char(string='Orden de Compra') invoiced_rejected = fields.Boolean(string='Factura Rechazada') supplier_ids = fields.One2many('rt.service.product.supplier', 'rt_deposito_product_id', 'Proveedores', copy=True) start = fields.Datetime('Inicio', required=True) stop = fields.Datetime('Fin', required=True) action_type_id = fields.Many2one('tipo.accion', string="Tipo de Acción") alquilado = fields.Boolean(string='Alquilado', track_visibility='always') partner_seller_id = fields.Many2one(comodel_name='res.partner', string='Vendedor', domain=[('seller', '=', True)], track_visibility='always') currency_id_vendedor = fields.Many2one(comodel_name='res.currency', string='Moneda', track_visibility='always') seller_commission = fields.Float(string='Comisión Vendedor', track_visibility='always') load_type = fields.Selection([('bulk', 'Bulk-Carga Suelta'), ('contenedor', 'Contenedor'), ('liquido_granel', u'Granel Líquido'), ('solido_granel', u'Granel Solido')], string='Tipo de Carga') container_type = fields.Many2one(comodel_name='fleet.vehicle', string='Tipo de Contenedor') container_number = fields.Char(string=u'Número de contenedor', size=13) make_container_number_invisible = fields.Boolean(string='Exception', default=False) container_number_exception = fields.Char( string=u'Nº de contenedor Excepción', size=13) valid_cointaner_number_text = fields.Boolean( help= 'Este booleano es para que se muestre un texto si el número de container es válido' ) invalid_cointaner_number_text = fields.Boolean( help= 'Este booleano es para que se muestre un texto si el número de container no es válido' ) @api.onchange('driver_commission') def carga_linea_comision(self): for rec in self: if rec.driver_commission: self.genera_comision_chofer(linea=rec, chofer=rec.driver_id) def genera_comision_chofer(self, linea, chofer): """ Se generan comisiones si se cumple la siguiente casuistica: Chofer Pertenece a categoria 'Camion Grande' A3 Producto = Flete Tipo de Accion = Viaje, Retiro de Vacío, Ingreso Cargado, Devolución de Vacío, Retiro de Cargado PARA CHOFERES DE CATEGORIA A2 (CAMION CHICO) LA COMISION ES EVENTUAL - NORMALMENTE NO CORRESPONDE :return: """ print( '-------------------------entro a la funcion genera_comision_chofer---------------------------------' ) flete = 'Flete' hr_job_obj = self.env['hr.job'] action_type_obj = self.env['tipo.accion'] categoria_corresponde_comision = hr_job_obj.search([ ('x_studio_categora_mtss', '=', 'A3 - Chofer de semirremolque') ]) categoria_comision_opcional = hr_job_obj.search([ ('x_studio_categora_mtss', '=', 'A2 - Chofer de Camión y Camioneta') ]) tipo_accion_corresponde_comision = action_type_obj.search([ ('corresponde_comision', '=', True) ]) if chofer.job_id.id in categoria_corresponde_comision.ids and linea.product_id.name == flete and linea.action_type_id.id in tipo_accion_corresponde_comision.ids: # Corresponde crear la comision linea.add_driver_commission() else: linea.add_driver_commission() @api.onchange('product_type', 'vehicle_id', 'matricula_dos_id') def _onchange_vehicle(self): """ Funcion temporaria que impide al usuario cargas vehiculos que no debe :return: """ domain = {} res = {} employee_obj = self.env['hr.employee'] condiciones_busqueda = [] condiciones_busqueda.append(('category_ids.name', '=', 'Chofer')) if condiciones_busqueda: employee = employee_obj.search(condiciones_busqueda) else: employee = employee_obj.search([]) fleet_obj = self.env['fleet.vehicle'] fleet_vehicle_id = fleet_obj.search([ ('state_id', 'in', ('Tractores', 'Camiones', 'Camionetas')) ]) fleet_matricula_dos_id = fleet_obj.search([ ('state_id', 'in', 'Semi Remolques y Remolques') ]) domain = { 'driver_id': [('id', 'in', employee.ids)], 'matricula_dos_id': [('id', 'in', fleet_matricula_dos_id.ids)], 'vehicle_id': [('id', 'in', fleet_vehicle_id.ids)], } if self.vehicle_id: if self.vehicle_id.state_id.name not in ('Tractores', 'Camiones', 'Camionetas'): self.vehicle_id = False raise Warning( 'El vehiculo debe tener una matricula \n No puede elegir un contenedor \n Selecione: Tractores, Camiones o Camionetas' ) if self.matricula_dos_id: if self.matricula_dos_id.state_id.name not in ( 'Semi Remolques y Remolques'): self.matricula_dos_id = False raise Warning( 'Solo puede selecionar Semi Remolques y Remolques') if self.vehicle_id: self.driver_id = self.vehicle_id.driver_id.id if not self.currency_id_chofer: self.currency_id_chofer = 46 if domain: res['domain'] = domain return res @api.onchange('container_number') def check_container_number(self): for rec in self: # Valida existencia if rec.container_number != False: # Valida largo correcto if len(rec.container_number) != 13: rec.container_number = False return { 'warning': { 'title': "Error", 'message': "Se espera un número de 13 cifras ej: BMOU-123456-7" } } # Valida existencia de - para poder realizar split if rec.container_number.count('-') != 2: rec.container_number = False return { 'warning': { 'title': "Error", 'message': "Formato inválido, se espera BMOU-123456-7" } } # letras_c,numeros_c,digitov_c = rec.container_number.split("-") Si es necesario utlizar para verificar otras cosas string_container, numeros_container, digitov_container = rec.container_number.split( "-") if not string_container.isalpha(): rec.container_number = False return { 'warning': { 'title': "Error", 'message': "Se espera un número de 13 cifras ej: BMOU-123456-7" } } try: type(int(numeros_container)) == int except ValueError: rec.container_number = False return { 'warning': { 'title': "Error", 'message': "Se espera un número de 13 cifras ej: BMOU-123456-7" } } try: type(int(numeros_container)) == int except ValueError: rec.container_number = False return { 'warning': { 'title': "Error", 'message': "Se espera un número de 13 cifras ej: BMOU-123456-7" } } try: type(int(digitov_container)) == int except ValueError: rec.container_number = False return { 'warning': { 'title': "Error", 'message': "Se espera un número de 13 cifras ej: BMOU-123456-7" } } # validar el numero con el algoritmo iso6346 container_number = rec.container_number.replace('-', '') if not iso6346.is_valid(container_number): rec.container_number = False rec.valid_cointaner_number_text = False rec.invalid_cointaner_number_text = True else: rec.valid_cointaner_number_text = True rec.invalid_cointaner_number_text = False @api.onchange('container_number_exception') def check_container_number_exception(self): for rec in self: # Valida existencia if rec.container_number_exception != False: # Valida largo correcto if len(rec.container_number_exception) != 13: rec.container_number_exception = False return { 'warning': { 'title': "Error", 'message': "Se espera un número de 13 cifras ej: BMOU-123456-7" } } # Valida existencia de - para poder realizar split if rec.container_number_exception.count('-') != 2: rec.container_number_exception = False return { 'warning': { 'title': "Error", 'message': "Formato inválido, se espera BMOU-123456-7" } } # letras_c,numeros_c,digitov_c = rec.container_number_exception.split("-") Si es necesario utlizar para verificar otras cosas string_container, numeros_container, digitov_container = rec.container_number_exception.split( "-") if not string_container.isalpha(): rec.container_number_exception = False return { 'warning': { 'title': "Error", 'message': "Se espera un número de 13 cifras ej: BMOU-123456-7" } } try: type(int(numeros_container)) == int except ValueError: rec.container_number_exception = False return { 'warning': { 'title': "Error", 'message': "Se espera un número de 13 cifras ej: BMOU-123456-7" } } try: type(int(numeros_container)) == int except ValueError: rec.container_number_exception = False return { 'warning': { 'title': "Error", 'message': "Se espera un número de 13 cifras ej: BMOU-123456-7" } } try: type(int(digitov_container)) == int except ValueError: rec.container_number_exception = False return { 'warning': { 'title': "Error", 'message': "Se espera un número de 13 cifras ej: BMOU-123456-7" } } @api.multi def get_container_number(self): for rec in self: rec.make_container_number_invisible = True return @api.multi def cancel_get_container_number(self): for rec in self: rec.make_container_number_invisible = False return @api.onchange('load_type') def _onchange_load_type(self): domain = {} warning = {} res = {} vehicles_obj = self.env['fleet.vehicle'] contenedores = vehicles_obj.search([('vehicle_type', '=', 'container') ]).ids domain = {'container_type': [('id', 'in', contenedores)]} if warning: res['warning'] = warning if domain: res['domain'] = domain return res @api.multi def add_supplier_to_product_line(self): if self._module: if self._module == 'deposito': tax_obj = self.env['account.tax'] taxes = tax_obj.search([('name', '=', 'IVA Directo Op Grav B') ]) if self.product_id.name == 'Alquiler': taxes = tax_obj.search([('name', '=', 'Compras Exentos IVA')]) lineas = [] for rec in self: line_dict = {} line_dict['deposito_id'] = rec.deposito_srv_id.id line_dict['supplier_id'] = rec.supplier_id.id line_dict['currency_id'] = rec.valor_compra_currency_id.id line_dict['amount'] = rec.valor_compra if self.product_id.name == 'Alquiler': line_dict['price_subtotal'] = rec.valor_compra else: line_dict['price_subtotal'] = rec.valor_compra * 1.22 line_dict['ref'] = self.deposito_srv_id.referencia line_dict['rt_service_id'] = False line_dict['rt_consol_product_id'] = False line_dict['rt_marfrig_product_id'] = False line_dict['rt_deposito_product_id'] = self.id line_dict['service_state'] = rec.state line_dict['tax_ids'] = [(6, 0, taxes.ids)] line_dict['service_date'] = rec.start # line_dict['tack_id'] = rec.container_number # line_dict['dua'] = self.get_dua() # line_dict['mic'] = ' ' line_dict['origin_id'] = rec.origin_id.id line_dict['destiny_id'] = rec.destiny_id.id line_dict['product_id'] = rec.product_id.id line_dict['output_reference'] = self.name line_dict[ 'partner_invoice_id'] = self.deposito_srv_id.partner_invoice_id.id lineas.append((0, 0, line_dict)) self.supplier_ids = lineas
class PurchaseOrder(models.Model): _inherit = 'purchase.order' @api.model def _default_picking_type(self): return self._get_picking_type(self.env.context.get('company_id') or self.env.company.id) incoterm_id = fields.Many2one('account.incoterms', 'Incoterm', states={'done': [('readonly', True)]}, help="International Commercial Terms are a series of predefined commercial terms used in international transactions.") incoming_picking_count = fields.Integer("Incoming Shipment count", compute='_compute_incoming_picking_count') picking_ids = fields.Many2many('stock.picking', compute='_compute_picking_ids', string='Receptions', copy=False, store=True) picking_type_id = fields.Many2one('stock.picking.type', 'Deliver To', states=Purchase.READONLY_STATES, required=True, default=_default_picking_type, domain="['|', ('warehouse_id', '=', False), ('warehouse_id.company_id', '=', company_id)]", help="This will determine operation type of incoming shipment") default_location_dest_id_usage = fields.Selection(related='picking_type_id.default_location_dest_id.usage', string='Destination Location Type', help="Technical field used to display the Drop Ship Address", readonly=True) group_id = fields.Many2one('procurement.group', string="Procurement Group", copy=False) is_shipped = fields.Boolean(compute="_compute_is_shipped") effective_date = fields.Datetime("Effective Date", compute='_compute_effective_date', store=True, copy=False, help="Completion date of the first receipt order.") on_time_rate = fields.Float(related='partner_id.on_time_rate', compute_sudo=False) @api.depends('order_line.move_ids.picking_id') def _compute_picking_ids(self): for order in self: order.picking_ids = order.order_line.move_ids.picking_id @api.depends('picking_ids') def _compute_incoming_picking_count(self): for order in self: order.incoming_picking_count = len(order.picking_ids) @api.depends('picking_ids.date_done') def _compute_effective_date(self): for order in self: pickings = order.picking_ids.filtered(lambda x: x.state == 'done' and x.location_dest_id.usage == 'internal' and x.date_done) order.effective_date = min(pickings.mapped('date_done'), default=False) @api.depends('picking_ids', 'picking_ids.state') def _compute_is_shipped(self): for order in self: if order.picking_ids and all(x.state in ['done', 'cancel'] for x in order.picking_ids): order.is_shipped = True else: order.is_shipped = False @api.onchange('picking_type_id') def _onchange_picking_type_id(self): if self.picking_type_id.default_location_dest_id.usage != 'customer': self.dest_address_id = False @api.onchange('company_id') def _onchange_company_id(self): p_type = self.picking_type_id if not(p_type and p_type.code == 'incoming' and (p_type.warehouse_id.company_id == self.company_id or not p_type.warehouse_id)): self.picking_type_id = self._get_picking_type(self.company_id.id) # -------------------------------------------------- # CRUD # -------------------------------------------------- def write(self, vals): if vals.get('order_line') and self.state == 'purchase': for order in self: pre_order_line_qty = {order_line: order_line.product_qty for order_line in order.mapped('order_line')} res = super(PurchaseOrder, self).write(vals) if vals.get('order_line') and self.state == 'purchase': for order in self: to_log = {} for order_line in order.order_line: if pre_order_line_qty.get(order_line, False) and float_compare(pre_order_line_qty[order_line], order_line.product_qty, precision_rounding=order_line.product_uom.rounding) > 0: to_log[order_line] = (order_line.product_qty, pre_order_line_qty[order_line]) if to_log: order._log_decrease_ordered_quantity(to_log) return res # -------------------------------------------------- # Actions # -------------------------------------------------- def button_approve(self, force=False): result = super(PurchaseOrder, self).button_approve(force=force) self._create_picking() return result def button_cancel(self): for order in self: for move in order.order_line.mapped('move_ids'): if move.state == 'done': raise UserError(_('Unable to cancel purchase order %s as some receptions have already been done.') % (order.name)) # If the product is MTO, change the procure_method of the closest move to purchase to MTS. # The purpose is to link the po that the user will manually generate to the existing moves's chain. if order.state in ('draft', 'sent', 'to approve', 'purchase'): for order_line in order.order_line: order_line.move_ids._action_cancel() if order_line.move_dest_ids: move_dest_ids = order_line.move_dest_ids if order_line.propagate_cancel: move_dest_ids._action_cancel() else: move_dest_ids.write({'procure_method': 'make_to_stock'}) move_dest_ids._recompute_state() for pick in order.picking_ids.filtered(lambda r: r.state != 'cancel'): pick.action_cancel() order.order_line.write({'move_dest_ids':[(5,0,0)]}) return super(PurchaseOrder, self).button_cancel() def action_view_picking(self): return self._get_action_view_picking(self.picking_ids) def _get_action_view_picking(self, pickings): """ This function returns an action that display existing picking orders of given purchase order ids. When only one found, show the picking immediately. """ self.ensure_one() result = self.env["ir.actions.actions"]._for_xml_id('stock.action_picking_tree_all') # override the context to get rid of the default filtering on operation type result['context'] = {'default_partner_id': self.partner_id.id, 'default_origin': self.name, 'default_picking_type_id': self.picking_type_id.id} # choose the view_mode accordingly if not pickings or len(pickings) > 1: result['domain'] = [('id', 'in', pickings.ids)] elif len(pickings) == 1: res = self.env.ref('stock.view_picking_form', False) form_view = [(res and res.id or False, 'form')] result['views'] = form_view + [(state, view) for state, view in result.get('views', []) if view != 'form'] result['res_id'] = pickings.id return result def _prepare_invoice(self): invoice_vals = super()._prepare_invoice() invoice_vals['invoice_incoterm_id'] = self.incoterm_id.id return invoice_vals # -------------------------------------------------- # Business methods # -------------------------------------------------- def _log_decrease_ordered_quantity(self, purchase_order_lines_quantities): def _keys_in_groupby(move): """ group by picking and the responsible for the product the move. """ return (move.picking_id, move.product_id.responsible_id) def _render_note_exception_quantity_po(order_exceptions): order_line_ids = self.env['purchase.order.line'].browse([order_line.id for order in order_exceptions.values() for order_line in order[0]]) purchase_order_ids = order_line_ids.mapped('order_id') move_ids = self.env['stock.move'].concat(*rendering_context.keys()) impacted_pickings = move_ids.mapped('picking_id')._get_impacted_pickings(move_ids) - move_ids.mapped('picking_id') values = { 'purchase_order_ids': purchase_order_ids, 'order_exceptions': order_exceptions.values(), 'impacted_pickings': impacted_pickings, } return self.env.ref('purchase_stock.exception_on_po')._render(values=values) documents = self.env['stock.picking']._log_activity_get_documents(purchase_order_lines_quantities, 'move_ids', 'DOWN', _keys_in_groupby) filtered_documents = {} for (parent, responsible), rendering_context in documents.items(): if parent._name == 'stock.picking': if parent.state == 'cancel': continue filtered_documents[(parent, responsible)] = rendering_context self.env['stock.picking']._log_activity(_render_note_exception_quantity_po, filtered_documents) def _get_destination_location(self): self.ensure_one() if self.dest_address_id: return self.dest_address_id.property_stock_customer.id return self.picking_type_id.default_location_dest_id.id @api.model def _get_picking_type(self, company_id): picking_type = self.env['stock.picking.type'].search([('code', '=', 'incoming'), ('warehouse_id.company_id', '=', company_id)]) if not picking_type: picking_type = self.env['stock.picking.type'].search([('code', '=', 'incoming'), ('warehouse_id', '=', False)]) return picking_type[:1] def _prepare_picking(self): if not self.group_id: self.group_id = self.group_id.create({ 'name': self.name, 'partner_id': self.partner_id.id }) if not self.partner_id.property_stock_supplier.id: raise UserError(_("You must set a Vendor Location for this partner %s", self.partner_id.name)) return { 'picking_type_id': self.picking_type_id.id, 'partner_id': self.partner_id.id, 'user_id': False, 'date': self.date_order, 'origin': self.name, 'location_dest_id': self._get_destination_location(), 'location_id': self.partner_id.property_stock_supplier.id, 'company_id': self.company_id.id, } def _create_picking(self): StockPicking = self.env['stock.picking'] for order in self.filtered(lambda po: po.state in ('purchase', 'done')): if any(product.type in ['product', 'consu'] for product in order.order_line.product_id): order = order.with_company(order.company_id) pickings = order.picking_ids.filtered(lambda x: x.state not in ('done', 'cancel')) if not pickings: res = order._prepare_picking() picking = StockPicking.with_user(SUPERUSER_ID).create(res) else: picking = pickings[0] moves = order.order_line._create_stock_moves(picking) moves = moves.filtered(lambda x: x.state not in ('done', 'cancel'))._action_confirm() seq = 0 for move in sorted(moves, key=lambda move: move.date): seq += 5 move.sequence = seq moves._action_assign() picking.message_post_with_view('mail.message_origin_link', values={'self': picking, 'origin': order}, subtype_id=self.env.ref('mail.mt_note').id) return True def _add_picking_info(self, activity): """Helper method to add picking info to the Date Updated activity when vender updates date_planned of the po lines. """ validated_picking = self.picking_ids.filtered(lambda p: p.state == 'done') if validated_picking: message = _("Those dates couldn’t be modified accordingly on the receipt %s which had already been validated.", validated_picking[0].name) elif not self.picking_ids: message = _("Corresponding receipt not found.") else: message = _("Those dates have been updated accordingly on the receipt %s.", self.picking_ids[0].name) activity.note += Markup('<p>{}</p>').format(message) def _create_update_date_activity(self, updated_dates): activity = super()._create_update_date_activity(updated_dates) self._add_picking_info(activity) def _update_update_date_activity(self, updated_dates, activity): # remove old picking info to update it note_lines = activity.note.split('<p>') note_lines.pop() activity.note = Markup('<p>').join(note_lines) super()._update_update_date_activity(updated_dates, activity) self._add_picking_info(activity) @api.model def _get_orders_to_remind(self): """When auto sending reminder mails, don't send for purchase order with validated receipts.""" return super()._get_orders_to_remind().filtered(lambda p: not p.effective_date)
class PosOrder(models.Model): _name = "pos.order" _description = "Point of Sale Orders" _order = "id desc" @api.model def _amount_line_tax(self, line, fiscal_position_id): taxes = line.tax_ids.filtered(lambda t: t.company_id.id == line.order_id.company_id.id) if fiscal_position_id: taxes = fiscal_position_id.map_tax(taxes, line.product_id, line.order_id.partner_id) price = line.price_unit * (1 - (line.discount or 0.0) / 100.0) taxes = taxes.compute_all(price, line.order_id.pricelist_id.currency_id, line.qty, product=line.product_id, partner=line.order_id.partner_id or False)['taxes'] return sum(tax.get('amount', 0.0) for tax in taxes) @api.model def _order_fields(self, ui_order): process_line = partial(self.env['pos.order.line']._order_line_fields, session_id=ui_order['pos_session_id']) return { 'name': ui_order['name'], 'user_id': ui_order['user_id'] or False, 'session_id': ui_order['pos_session_id'], 'lines': [process_line(l) for l in ui_order['lines']] if ui_order['lines'] else False, 'pos_reference': ui_order['name'], 'partner_id': ui_order['partner_id'] or False, 'date_order': ui_order['creation_date'], 'fiscal_position_id': ui_order['fiscal_position_id'] } def _payment_fields(self, ui_paymentline): return { 'amount': ui_paymentline['amount'] or 0.0, 'payment_date': ui_paymentline['name'], 'statement_id': ui_paymentline['statement_id'], 'payment_name': ui_paymentline.get('note', False), 'journal': ui_paymentline['journal_id'], } # This deals with orders that belong to a closed session. In order # to recover from this situation we create a new rescue session, # making it obvious that something went wrong. # A new, separate, rescue session is preferred for every such recovery, # to avoid adding unrelated orders to live sessions. def _get_valid_session(self, order): PosSession = self.env['pos.session'] closed_session = PosSession.browse(order['pos_session_id']) _logger.warning('session %s (ID: %s) was closed but received order %s (total: %s) belonging to it', closed_session.name, closed_session.id, order['name'], order['amount_total']) rescue_session = PosSession.search([ ('state', 'not in', ('closed', 'closing_control')), ('rescue', '=', True), ('config_id', '=', closed_session.config_id.id), ], limit=1) if rescue_session: _logger.warning('reusing recovery session %s for saving order %s', rescue_session.name, order['name']) return rescue_session _logger.warning('attempting to create recovery session for saving order %s', order['name']) new_session = PosSession.create({ 'config_id': closed_session.config_id.id, 'name': _('(RESCUE FOR %(session)s)') % {'session': closed_session.name}, 'rescue': True, # avoid conflict with live sessions }) # bypass opening_control (necessary when using cash control) new_session.action_pos_session_open() return new_session def _match_payment_to_invoice(self, order): account_precision = self.env['decimal.precision'].precision_get('Account') # ignore orders with an amount_paid of 0 because those are returns through the POS if not float_is_zero(order['amount_return'], account_precision) and not float_is_zero(order['amount_paid'], account_precision): cur_amount_paid = 0 payments_to_keep = [] for payment in order.get('statement_ids'): if cur_amount_paid + payment[2]['amount'] > order['amount_total']: payment[2]['amount'] = order['amount_total'] - cur_amount_paid payments_to_keep.append(payment) break cur_amount_paid += payment[2]['amount'] payments_to_keep.append(payment) order['statement_ids'] = payments_to_keep order['amount_return'] = 0 @api.model def _process_order(self, pos_order): prec_acc = self.env['decimal.precision'].precision_get('Account') pos_session = self.env['pos.session'].browse(pos_order['pos_session_id']) if pos_session.state == 'closing_control' or pos_session.state == 'closed': pos_order['pos_session_id'] = self._get_valid_session(pos_order).id order = self.create(self._order_fields(pos_order)) journal_ids = set() for payments in pos_order['statement_ids']: if not float_is_zero(payments[2]['amount'], precision_digits=prec_acc): order.add_payment(self._payment_fields(payments[2])) journal_ids.add(payments[2]['journal_id']) if pos_session.sequence_number <= pos_order['sequence_number']: pos_session.write({'sequence_number': pos_order['sequence_number'] + 1}) pos_session.refresh() if not float_is_zero(pos_order['amount_return'], prec_acc): cash_journal_id = pos_session.cash_journal_id.id if not cash_journal_id: # Select for change one of the cash journals used in this # payment cash_journal = self.env['account.journal'].search([ ('type', '=', 'cash'), ('id', 'in', list(journal_ids)), ], limit=1) if not cash_journal: # If none, select for change one of the cash journals of the POS # This is used for example when a customer pays by credit card # an amount higher than total amount of the order and gets cash back cash_journal = [statement.journal_id for statement in pos_session.statement_ids if statement.journal_id.type == 'cash'] if not cash_journal: raise UserError(_("No cash statement found for this session. Unable to record returned cash.")) cash_journal_id = cash_journal[0].id order.add_payment({ 'amount': -pos_order['amount_return'], 'payment_date': fields.Datetime.now(), 'payment_name': _('return'), 'journal': cash_journal_id, }) return order def _prepare_analytic_account(self, line): '''This method is designed to be inherited in a custom module''' return False def _create_account_move(self, dt, ref, journal_id, company_id): date_tz_user = fields.Datetime.context_timestamp(self, fields.Datetime.from_string(dt)) date_tz_user = fields.Date.to_string(date_tz_user) return self.env['account.move'].sudo().create({'ref': ref, 'journal_id': journal_id, 'date': date_tz_user}) def _prepare_invoice(self): """ Prepare the dict of values to create the new invoice for a pos order. """ return { 'name': self.name, 'origin': self.name, 'account_id': self.partner_id.property_account_receivable_id.id, 'journal_id': self.session_id.config_id.invoice_journal_id.id, 'type': 'out_invoice', 'reference': self.name, 'partner_id': self.partner_id.id, 'comment': self.note or '', # considering partner's sale pricelist's currency 'currency_id': self.pricelist_id.currency_id.id, 'user_id': self.env.uid, } def _action_create_invoice_line(self, line=False, invoice_id=False): InvoiceLine = self.env['account.invoice.line'] inv_name = line.product_id.name_get()[0][1] inv_line = { 'invoice_id': invoice_id, 'product_id': line.product_id.id, 'quantity': line.qty, 'account_analytic_id': self._prepare_analytic_account(line), 'name': inv_name, } # Oldlin trick invoice_line = InvoiceLine.sudo().new(inv_line) invoice_line._onchange_product_id() invoice_line.invoice_line_tax_ids = invoice_line.invoice_line_tax_ids.filtered(lambda t: t.company_id.id == line.order_id.company_id.id).ids fiscal_position_id = line.order_id.fiscal_position_id if fiscal_position_id: invoice_line.invoice_line_tax_ids = fiscal_position_id.map_tax(invoice_line.invoice_line_tax_ids, line.product_id, line.order_id.partner_id) invoice_line.invoice_line_tax_ids = invoice_line.invoice_line_tax_ids.ids # We convert a new id object back to a dictionary to write to # bridge between old and new api inv_line = invoice_line._convert_to_write({name: invoice_line[name] for name in invoice_line._cache}) inv_line.update(price_unit=line.price_unit, discount=line.discount) return InvoiceLine.sudo().create(inv_line) def _create_account_move_line(self, session=None, move=None): # Tricky, via the workflow, we only have one id in the ids variable """Create a account move line of order grouped by products or not.""" IrProperty = self.env['ir.property'] ResPartner = self.env['res.partner'] if session and not all(session.id == order.session_id.id for order in self): raise UserError(_('Selected orders do not have the same session!')) grouped_data = {} have_to_group_by = session and session.config_id.group_by or False rounding_method = session and session.config_id.company_id.tax_calculation_rounding_method for order in self.filtered(lambda o: not o.account_move or order.state == 'paid'): current_company = order.sale_journal.company_id account_def = IrProperty.get( 'property_account_receivable_id', 'res.partner') order_account = order.partner_id.property_account_receivable_id.id or account_def and account_def.id partner_id = ResPartner._find_accounting_partner(order.partner_id).id or False if move is None: # Create an entry for the sale journal_id = self.env['ir.config_parameter'].sudo().get_param( 'pos.closing.journal_id_%s' % current_company.id, default=order.sale_journal.id) move = self._create_account_move( order.session_id.start_at, order.name, int(journal_id), order.company_id.id) def insert_data(data_type, values): # if have_to_group_by: values.update({ 'partner_id': partner_id, 'move_id': move.id, }) if data_type == 'product': key = ('product', values['partner_id'], (values['product_id'], tuple(values['tax_ids'][0][2]), values['name']), values['analytic_account_id'], values['debit'] > 0) elif data_type == 'tax': key = ('tax', values['partner_id'], values['tax_line_id'], values['debit'] > 0) elif data_type == 'counter_part': key = ('counter_part', values['partner_id'], values['account_id'], values['debit'] > 0) else: return grouped_data.setdefault(key, []) if have_to_group_by: if not grouped_data[key]: grouped_data[key].append(values) else: current_value = grouped_data[key][0] current_value['quantity'] = current_value.get('quantity', 0.0) + values.get('quantity', 0.0) current_value['credit'] = current_value.get('credit', 0.0) + values.get('credit', 0.0) current_value['debit'] = current_value.get('debit', 0.0) + values.get('debit', 0.0) else: grouped_data[key].append(values) # because of the weird way the pos order is written, we need to make sure there is at least one line, # because just after the 'for' loop there are references to 'line' and 'income_account' variables (that # are set inside the for loop) # TOFIX: a deep refactoring of this method (and class!) is needed # in order to get rid of this stupid hack assert order.lines, _('The POS order must have lines when calling this method') # Create an move for each order line cur = order.pricelist_id.currency_id for line in order.lines: amount = line.price_subtotal # Search for the income account if line.product_id.property_account_income_id.id: income_account = line.product_id.property_account_income_id.id elif line.product_id.categ_id.property_account_income_categ_id.id: income_account = line.product_id.categ_id.property_account_income_categ_id.id else: raise UserError(_('Please define income ' 'account for this product: "%s" (id:%d).') % (line.product_id.name, line.product_id.id)) name = line.product_id.name if line.notice: # add discount reason in move name = name + ' (' + line.notice + ')' # Create a move for the line for the order line insert_data('product', { 'name': name, 'quantity': line.qty, 'product_id': line.product_id.id, 'account_id': income_account, 'analytic_account_id': self._prepare_analytic_account(line), 'credit': ((amount > 0) and amount) or 0.0, 'debit': ((amount < 0) and -amount) or 0.0, 'tax_ids': [(6, 0, line.tax_ids_after_fiscal_position.ids)], 'partner_id': partner_id }) # Create the tax lines taxes = line.tax_ids_after_fiscal_position.filtered(lambda t: t.company_id.id == current_company.id) if not taxes: continue for tax in taxes.compute_all(line.price_unit * (100.0 - line.discount) / 100.0, cur, line.qty)['taxes']: insert_data('tax', { 'name': _('Tax') + ' ' + tax['name'], 'product_id': line.product_id.id, 'quantity': line.qty, 'account_id': tax['account_id'] or income_account, 'credit': ((tax['amount'] > 0) and tax['amount']) or 0.0, 'debit': ((tax['amount'] < 0) and -tax['amount']) or 0.0, 'tax_line_id': tax['id'], 'partner_id': partner_id }) # round tax lines per order if rounding_method == 'round_globally': for group_key, group_value in grouped_data.iteritems(): if group_key[0] == 'tax': for line in group_value: line['credit'] = cur.round(line['credit']) line['debit'] = cur.round(line['debit']) # counterpart insert_data('counter_part', { 'name': _("Trade Receivables"), # order.name, 'account_id': order_account, 'credit': ((order.amount_total < 0) and -order.amount_total) or 0.0, 'debit': ((order.amount_total > 0) and order.amount_total) or 0.0, 'partner_id': partner_id }) order.write({'state': 'done', 'account_move': move.id}) all_lines = [] for group_key, group_data in grouped_data.iteritems(): for value in group_data: all_lines.append((0, 0, value),) if move: # In case no order was changed move.sudo().write({'line_ids': all_lines}) move.sudo().post() return True def _reconcile_payments(self): for order in self: aml = order.statement_ids.mapped('journal_entry_ids') | order.account_move.line_ids aml = aml.filtered(lambda r: not r.reconciled and r.account_id.internal_type == 'receivable' and r.partner_id == order.partner_id) try: aml.reconcile() except: # There might be unexpected situations where the automatic reconciliation won't # work. We don't want the user to be blocked because of this, since the automatic # reconciliation is introduced for convenience, not for mandatory accounting # reasons. continue def _default_session(self): return self.env['pos.session'].search([('state', '=', 'opened'), ('user_id', '=', self.env.uid)], limit=1) def _default_pricelist(self): return self._default_session().config_id.pricelist_id name = fields.Char(string='Order Ref', required=True, readonly=True, copy=False, default='/') company_id = fields.Many2one('res.company', string='Company', required=True, readonly=True, default=lambda self: self.env.user.company_id) date_order = fields.Datetime(string='Order Date', readonly=True, index=True, default=fields.Datetime.now) user_id = fields.Many2one( comodel_name='res.users', string='Salesman', help="Person who uses the cash register. It can be a reliever, a student or an interim employee.", default=lambda self: self.env.uid, states={'done': [('readonly', True)], 'invoiced': [('readonly', True)]}, ) amount_tax = fields.Float(compute='_compute_amount_all', string='Taxes', digits=0) amount_total = fields.Float(compute='_compute_amount_all', string='Total', digits=0) amount_paid = fields.Float(compute='_compute_amount_all', string='Paid', states={'draft': [('readonly', False)]}, readonly=True, digits=0) amount_return = fields.Float(compute='_compute_amount_all', string='Returned', digits=0) lines = fields.One2many('pos.order.line', 'order_id', string='Order Lines', states={'draft': [('readonly', False)]}, readonly=True, copy=True) statement_ids = fields.One2many('account.bank.statement.line', 'pos_statement_id', string='Payments', states={'draft': [('readonly', False)]}, readonly=True) pricelist_id = fields.Many2one('product.pricelist', string='Pricelist', required=True, states={ 'draft': [('readonly', False)]}, readonly=True, default=_default_pricelist) partner_id = fields.Many2one('res.partner', string='Customer', change_default=True, index=True, states={'draft': [('readonly', False)], 'paid': [('readonly', False)]}) sequence_number = fields.Integer(string='Sequence Number', help='A session-unique sequence number for the order', default=1) session_id = fields.Many2one( 'pos.session', string='Session', required=True, index=True, domain="[('state', '=', 'opened')]", states={'draft': [('readonly', False)]}, readonly=True, default=_default_session) config_id = fields.Many2one('pos.config', related='session_id.config_id', string="Point of Sale") state = fields.Selection( [('draft', 'New'), ('cancel', 'Cancelled'), ('paid', 'Paid'), ('done', 'Posted'), ('invoiced', 'Invoiced')], 'Status', readonly=True, copy=False, default='draft') invoice_id = fields.Many2one('account.invoice', string='Invoice', copy=False) account_move = fields.Many2one('account.move', string='Journal Entry', readonly=True, copy=False) picking_id = fields.Many2one('stock.picking', string='Picking', readonly=True, copy=False) picking_type_id = fields.Many2one('stock.picking.type', related='session_id.config_id.picking_type_id', string="Operation Type") location_id = fields.Many2one( comodel_name='stock.location', related='session_id.config_id.stock_location_id', string="Location", store=True, readonly=True, ) note = fields.Text(string='Internal Notes') nb_print = fields.Integer(string='Number of Print', readonly=True, copy=False, default=0) pos_reference = fields.Char(string='Receipt Ref', readonly=True, copy=False) sale_journal = fields.Many2one('account.journal', related='session_id.config_id.journal_id', string='Sales Journal', store=True, readonly=True) fiscal_position_id = fields.Many2one( comodel_name='account.fiscal.position', string='Fiscal Position', default=lambda self: self._default_session().config_id.default_fiscal_position_id, readonly=True, states={'draft': [('readonly', False)]}, ) @api.depends('statement_ids', 'lines.price_subtotal_incl', 'lines.discount') def _compute_amount_all(self): for order in self: order.amount_paid = order.amount_return = order.amount_tax = 0.0 currency = order.pricelist_id.currency_id order.amount_paid = sum(payment.amount for payment in order.statement_ids) order.amount_return = sum(payment.amount < 0 and payment.amount or 0 for payment in order.statement_ids) order.amount_tax = currency.round(sum(self._amount_line_tax(line, order.fiscal_position_id) for line in order.lines)) amount_untaxed = currency.round(sum(line.price_subtotal for line in order.lines)) order.amount_total = order.amount_tax + amount_untaxed @api.onchange('partner_id') def _onchange_partner_id(self): if self.partner_id: self.pricelist = self.partner_id.property_product_pricelist.id @api.multi def write(self, vals): res = super(PosOrder, self).write(vals) Partner = self.env['res.partner'] # If you change the partner of the PoS order, change also the partner of the associated bank statement lines if 'partner_id' in vals: for order in self: partner_id = False if order.invoice_id: raise UserError(_("You cannot change the partner of a POS order for which an invoice has already been issued.")) if vals['partner_id']: partner = Partner.browse(vals['partner_id']) partner_id = Partner._find_accounting_partner(partner).id order.statement_ids.write({'partner_id': partner_id}) return res @api.multi def unlink(self): for pos_order in self.filtered(lambda pos_order: pos_order.state not in ['draft', 'cancel']): raise UserError(_('In order to delete a sale, it must be new or cancelled.')) return super(PosOrder, self).unlink() @api.model def create(self, values): if values.get('session_id'): # set name based on the sequence specified on the config session = self.env['pos.session'].browse(values['session_id']) values['name'] = session.config_id.sequence_id._next() values.setdefault('pricelist_id', session.config_id.pricelist_id.id) else: # fallback on any pos.order sequence values['name'] = self.env['ir.sequence'].next_by_code('pos.order') return super(PosOrder, self).create(values) @api.multi def action_view_invoice(self): return { 'name': _('Customer Invoice'), 'view_mode': 'form', 'view_id': self.env.ref('account.invoice_form').id, 'res_model': 'account.invoice', 'context': "{'type':'out_invoice'}", 'type': 'ir.actions.act_window', 'res_id': self.invoice_id.id, } @api.multi def action_pos_order_paid(self): if not self.test_paid(): raise UserError(_("Order is not paid.")) self.write({'state': 'paid'}) return self.create_picking() @api.multi def action_pos_order_invoice(self): Invoice = self.env['account.invoice'] for order in self: # Force company for all SUPERUSER_ID action local_context = dict(self.env.context, force_company=order.company_id.id, company_id=order.company_id.id) if order.invoice_id: Invoice += order.invoice_id continue if not order.partner_id: raise UserError(_('Please provide a partner for the sale.')) invoice = Invoice.new(order._prepare_invoice()) invoice._onchange_partner_id() invoice.fiscal_position_id = order.fiscal_position_id inv = invoice._convert_to_write({name: invoice[name] for name in invoice._cache}) new_invoice = Invoice.with_context(local_context).sudo().create(inv) message = _("This invoice has been created from the point of sale session: <a href=# data-oe-model=pos.order data-oe-id=%d>%s</a>") % (order.id, order.name) new_invoice.message_post(body=message) order.write({'invoice_id': new_invoice.id, 'state': 'invoiced'}) Invoice += new_invoice for line in order.lines: self.with_context(local_context)._action_create_invoice_line(line, new_invoice.id) new_invoice.with_context(local_context).sudo().compute_taxes() order.sudo().write({'state': 'invoiced'}) if not Invoice: return {} return { 'name': _('Customer Invoice'), 'view_type': 'form', 'view_mode': 'form', 'view_id': self.env.ref('account.invoice_form').id, 'res_model': 'account.invoice', 'context': "{'type':'out_invoice'}", 'type': 'ir.actions.act_window', 'nodestroy': True, 'target': 'current', 'res_id': Invoice and Invoice.ids[0] or False, } # this method is unused, and so is the state 'cancel' @api.multi def action_pos_order_cancel(self): return self.write({'state': 'cancel'}) @api.multi def action_pos_order_done(self): return self._create_account_move_line() @api.model def create_from_ui(self, orders): # Keep only new orders submitted_references = [o['data']['name'] for o in orders] pos_order = self.search([('pos_reference', 'in', submitted_references)]) existing_orders = pos_order.read(['pos_reference']) existing_references = set([o['pos_reference'] for o in existing_orders]) orders_to_save = [o for o in orders if o['data']['name'] not in existing_references] order_ids = [] for tmp_order in orders_to_save: to_invoice = tmp_order['to_invoice'] order = tmp_order['data'] if to_invoice: self._match_payment_to_invoice(order) pos_order = self._process_order(order) order_ids.append(pos_order.id) try: pos_order.action_pos_order_paid() except psycopg2.OperationalError: # do not hide transactional errors, the order(s) won't be saved! raise except Exception as e: _logger.error('Could not fully process the POS Order: %s', tools.ustr(e)) if to_invoice: pos_order.action_pos_order_invoice() pos_order.invoice_id.sudo().action_invoice_open() pos_order.account_move = pos_order.invoice_id.move_id return order_ids def test_paid(self): """A Point of Sale is paid when the sum @return: True """ for order in self: if order.lines and not order.amount_total: continue if (not order.lines) or (not order.statement_ids) or (abs(order.amount_total - order.amount_paid) > 0.00001): return False return True def create_picking(self): """Create a picking for each order and validate it.""" Picking = self.env['stock.picking'] Move = self.env['stock.move'] StockWarehouse = self.env['stock.warehouse'] for order in self: if not order.lines.filtered(lambda l: l.product_id.type in ['product', 'consu']): continue address = order.partner_id.address_get(['delivery']) or {} picking_type = order.picking_type_id return_pick_type = order.picking_type_id.return_picking_type_id or order.picking_type_id order_picking = Picking return_picking = Picking moves = Move location_id = order.location_id.id if order.partner_id: destination_id = order.partner_id.property_stock_customer.id else: if (not picking_type) or (not picking_type.default_location_dest_id): customerloc, supplierloc = StockWarehouse._get_partner_locations() destination_id = customerloc.id else: destination_id = picking_type.default_location_dest_id.id if picking_type: message = _("This transfer has been created from the point of sale session: <a href=# data-oe-model=pos.order data-oe-id=%d>%s</a>") % (order.id, order.name) picking_vals = { 'origin': order.name, 'partner_id': address.get('delivery', False), 'date_done': order.date_order, 'picking_type_id': picking_type.id, 'company_id': order.company_id.id, 'move_type': 'direct', 'note': order.note or "", 'location_id': location_id, 'location_dest_id': destination_id, } pos_qty = any([x.qty > 0 for x in order.lines]) if pos_qty: order_picking = Picking.create(picking_vals.copy()) order_picking.message_post(body=message) neg_qty = any([x.qty < 0 for x in order.lines]) if neg_qty: return_vals = picking_vals.copy() return_vals.update({ 'location_id': destination_id, 'location_dest_id': return_pick_type != picking_type and return_pick_type.default_location_dest_id.id or location_id, 'picking_type_id': return_pick_type.id }) return_picking = Picking.create(return_vals) return_picking.message_post(body=message) for line in order.lines.filtered(lambda l: l.product_id.type in ['product', 'consu'] and l.qty != 0): moves |= Move.create({ 'name': line.name, 'product_uom': line.product_id.uom_id.id, 'picking_id': order_picking.id if line.qty >= 0 else return_picking.id, 'picking_type_id': picking_type.id if line.qty >= 0 else return_pick_type.id, 'product_id': line.product_id.id, 'product_uom_qty': abs(line.qty), 'state': 'draft', 'location_id': location_id if line.qty >= 0 else destination_id, 'location_dest_id': destination_id if line.qty >= 0 else return_pick_type != picking_type and return_pick_type.default_location_dest_id.id or location_id, }) # prefer associating the regular order picking, not the return order.write({'picking_id': order_picking.id or return_picking.id}) if return_picking: order._force_picking_done(return_picking) if order_picking: order._force_picking_done(order_picking) # when the pos.config has no picking_type_id set only the moves will be created if moves and not return_picking and not order_picking: moves.action_assign() moves.filtered(lambda m: m.state in ['confirmed', 'waiting']).force_assign() moves.filtered(lambda m: m.product_id.tracking == 'none').action_done() return True def _force_picking_done(self, picking): """Force picking in order to be set as done.""" self.ensure_one() picking.action_assign() picking.force_assign() wrong_lots = self.set_pack_operation_lot(picking) if not wrong_lots: picking.action_done() def set_pack_operation_lot(self, picking=None): """Set Serial/Lot number in pack operations to mark the pack operation done.""" StockProductionLot = self.env['stock.production.lot'] PosPackOperationLot = self.env['pos.pack.operation.lot'] has_wrong_lots = False for order in self: for pack_operation in (picking or self.picking_id).pack_operation_ids: picking_type = (picking or self.picking_id).picking_type_id lots_necessary = True if picking_type: lots_necessary = picking_type and picking_type.use_existing_lots qty = 0 qty_done = 0 pack_lots = [] pos_pack_lots = PosPackOperationLot.search([('order_id', '=', order.id), ('product_id', '=', pack_operation.product_id.id)]) pack_lot_names = [pos_pack.lot_name for pos_pack in pos_pack_lots] if pack_lot_names and lots_necessary: for lot_name in list(set(pack_lot_names)): stock_production_lot = StockProductionLot.search([('name', '=', lot_name), ('product_id', '=', pack_operation.product_id.id)]) if stock_production_lot: if stock_production_lot.product_id.tracking == 'lot': # if a lot nr is set through the frontend it will refer to the full quantity qty = pack_operation.product_qty else: # serial numbers qty = 1.0 qty_done += qty pack_lots.append({'lot_id': stock_production_lot.id, 'qty': qty}) else: has_wrong_lots = True elif pack_operation.product_id.tracking == 'none' or not lots_necessary: qty_done = pack_operation.product_qty else: has_wrong_lots = True pack_operation.write({'pack_lot_ids': map(lambda x: (0, 0, x), pack_lots), 'qty_done': qty_done}) return has_wrong_lots def add_payment(self, data): """Create a new payment for the order""" args = { 'amount': data['amount'], 'date': data.get('payment_date', fields.Date.today()), 'name': self.name + ': ' + (data.get('payment_name', '') or ''), 'partner_id': self.env["res.partner"]._find_accounting_partner(self.partner_id).id or False, } journal_id = data.get('journal', False) statement_id = data.get('statement_id', False) assert journal_id or statement_id, "No statement_id or journal_id passed to the method!" journal = self.env['account.journal'].browse(journal_id) # use the company of the journal and not of the current user company_cxt = dict(self.env.context, force_company=journal.company_id.id) account_def = self.env['ir.property'].with_context(company_cxt).get('property_account_receivable_id', 'res.partner') args['account_id'] = (self.partner_id.property_account_receivable_id.id) or (account_def and account_def.id) or False if not args['account_id']: if not args['partner_id']: msg = _('There is no receivable account defined to make payment.') else: msg = _('There is no receivable account defined to make payment for the partner: "%s" (id:%d).') % ( self.partner_id.name, self.partner_id.id,) raise UserError(msg) context = dict(self.env.context) context.pop('pos_session_id', False) for statement in self.session_id.statement_ids: if statement.id == statement_id: journal_id = statement.journal_id.id break elif statement.journal_id.id == journal_id: statement_id = statement.id break if not statement_id: raise UserError(_('You have to open at least one cashbox.')) args.update({ 'statement_id': statement_id, 'pos_statement_id': self.id, 'journal_id': journal_id, 'ref': self.session_id.name, }) self.env['account.bank.statement.line'].with_context(context).create(args) return statement_id @api.multi def refund(self): """Create a copy of order for refund order""" PosOrder = self.env['pos.order'] current_session = self.env['pos.session'].search([('state', '!=', 'closed'), ('user_id', '=', self.env.uid)], limit=1) if not current_session: raise UserError(_('To return product(s), you need to open a session that will be used to register the refund.')) for order in self: clone = order.copy({ # ot used, name forced by create 'name': order.name + _(' REFUND'), 'session_id': current_session.id, 'date_order': fields.Datetime.now(), 'pos_reference': order.pos_reference, 'lines': False, }) for line in order.lines: clone_line = line.copy({ # required=True, copy=False 'name': line.name + _(' REFUND'), 'order_id': clone.id, 'qty': -line.qty, }) PosOrder += clone return { 'name': _('Return Products'), 'view_type': 'form', 'view_mode': 'form', 'res_model': 'pos.order', 'res_id': PosOrder.ids[0], 'view_id': False, 'context': self.env.context, 'type': 'ir.actions.act_window', 'target': 'current', }
class HrContract(models.Model): _inherit = 'hr.contract' transport_mode = fields.Selection( [ ('company_car', 'Company car'), ('public_transport', 'Public Transport'), ('others', 'Other'), ], string="Transport", default='company_car', help="Transport mode the employee uses to go to work.") car_atn = fields.Monetary(string='ATN Company Car') public_transport_employee_amount = fields.Monetary( 'Paid by the employee (Monthly)') thirteen_month = fields.Monetary( compute='_compute_holidays_advantages', string='13th Month', help="Yearly gross amount the employee receives as 13th month bonus.") double_holidays = fields.Monetary( compute='_compute_holidays_advantages', string='Holiday Bonus', help="Yearly gross amount the employee receives as holidays bonus.") warrant_value_employee = fields.Monetary( compute='_compute_warrants_cost', string="Warrant value for the employee") # Employer costs fields final_yearly_costs = fields.Monetary( compute='_compute_final_yearly_costs', readonly=False, string='Total Employee Cost', groups="hr.group_hr_manager", help="Total yearly cost of the employee for the employer.") monthly_yearly_costs = fields.Monetary( compute='_compute_monthly_yearly_costs', string='Monthly Equivalent Cost', readonly=True, help="Total monthly cost of the employee for the employer.") ucm_insurance = fields.Monetary(compute='_compute_ucm_insurance', string="Social Secretary Costs") social_security_contributions = fields.Monetary( compute='_compute_social_security_contributions', string="Social Security Contributions") yearly_cost_before_charges = fields.Monetary( compute='_compute_yearly_cost_before_charges', string="Yearly Costs Before Charges") meal_voucher_paid_by_employer = fields.Monetary( compute='_compute_meal_voucher_paid_by_employer', string="Meal Voucher Paid by Employer") company_car_total_depreciated_cost = fields.Monetary() public_transport_reimbursed_amount = fields.Monetary( string='Reimbursed amount', compute='_compute_public_transport_reimbursed_amount', readonly=False, store=True) others_reimbursed_amount = fields.Monetary(string='Reimbursed amount') transport_employer_cost = fields.Monetary( compute='_compute_transport_employer_cost', string="Employer cost from employee transports") warrants_cost = fields.Monetary(compute='_compute_warrants_cost') # Advantages commission_on_target = fields.Monetary( string="Commission on Target", default=lambda self: self.get_attribute('commission_on_target', 'default_value'), help= "Monthly gross amount that the employee receives if the target is reached." ) fuel_card = fields.Monetary( string="Fuel Card", default=lambda self: self.get_attribute('fuel_card', 'default_value'), help="Monthly amount the employee receives on his fuel card.") internet = fields.Monetary( string="Internet", default=lambda self: self.get_attribute('internet', 'default_value'), help= "The employee's internet subcription will be paid up to this amount.") representation_fees = fields.Monetary( string="Representation Fees", default=lambda self: self.get_attribute('representation_fees', 'default_value'), help= "Monthly net amount the employee receives to cover his representation fees." ) mobile = fields.Monetary( string="Mobile", default=lambda self: self.get_attribute('mobile', 'default_value'), help= "The employee's mobile subscription will be paid up to this amount.") mobile_plus = fields.Monetary( string="International Communication", default=lambda self: self.get_attribute('mobile_plus', 'default_value' ), help= "The employee's mobile subscription for international communication will be paid up to this amount." ) meal_voucher_amount = fields.Monetary( string="Meal Vouchers", default=lambda self: self.get_attribute('meal_voucher_amount', 'default_value'), help= "Amount the employee receives in the form of meal vouchers per worked day." ) holidays = fields.Float( string='Legal Leaves', default=lambda self: self.get_attribute('holidays', 'default_value'), help="Number of days of paid leaves the employee gets per year.") holidays_editable = fields.Boolean(string="Editable Leaves", default=True) holidays_compensation = fields.Monetary( compute='_compute_holidays_compensation', string="Holidays Compensation") wage_with_holidays = fields.Monetary( compute='_compute_wage_with_holidays', inverse='_inverse_wage_with_holidays', string="Wage update with holidays retenues") additional_net_amount = fields.Monetary( string="Net Supplements", help="Monthly net amount the employee receives.") retained_net_amount = fields.Monetary( sting="Net Retained", help="Monthly net amount that is retained on the employee's salary.") eco_checks = fields.Monetary( "Eco Vouchers", default=lambda self: self.get_attribute('eco_checks', 'default_value'), help="Yearly amount the employee receives in the form of eco vouchers." ) @api.depends('holidays', 'wage', 'final_yearly_costs') def _compute_wage_with_holidays(self): for contract in self: if contract.holidays > 20.0: yearly_cost = contract.final_yearly_costs * ( 1.0 - (contract.holidays - 20.0) / 231.0) contract.wage_with_holidays = contract._get_gross_from_employer_costs( yearly_cost) else: contract.wage_with_holidays = contract.wage def _inverse_wage_with_holidays(self): for contract in self: if contract.holidays > 20.0: remaining_for_gross = contract.wage_with_holidays * ( 13.0 + 13.0 * 0.3507 + 0.92) yearly_cost = remaining_for_gross \ + 12.0 * contract.representation_fees \ + 12.0 * contract.fuel_card \ + 12.0 * contract.internet \ + 12.0 * (contract.mobile + contract.mobile_plus) \ + 12.0 * contract.transport_employer_cost \ + contract.warrants_cost \ + 220.0 * contract.meal_voucher_paid_by_employer contract.final_yearly_costs = yearly_cost / ( 1.0 - (contract.holidays - 20.0) / 231.0) contract.wage = contract._get_gross_from_employer_costs( contract.final_yearly_costs) else: contract.wage = contract.wage_with_holidays @api.depends('transport_mode', 'company_car_total_depreciated_cost', 'public_transport_reimbursed_amount', 'others_reimbursed_amount') def _compute_transport_employer_cost(self): for contract in self: if contract.transport_mode == 'company_car': contract.transport_employer_cost = contract.company_car_total_depreciated_cost elif contract.transport_mode == 'public_transport': contract.transport_employer_cost = contract.public_transport_reimbursed_amount elif contract.transport_mode == 'others': contract.transport_employer_cost = contract.others_reimbursed_amount @api.depends('commission_on_target') def _compute_warrants_cost(self): for contract in self: contract.warrants_cost = contract.commission_on_target * 1.326 * 1.05 * 12.0 contract.warrant_value_employee = contract.commission_on_target * 1.326 * ( 1.00 - 0.535) * 12.0 @api.depends('wage', 'fuel_card', 'representation_fees', 'transport_employer_cost', 'internet', 'mobile', 'mobile_plus') def _compute_yearly_cost_before_charges(self): for contract in self: contract.yearly_cost_before_charges = 12.0 * ( contract.wage * (1.0 + 1.0 / 12.0) + contract.fuel_card + contract.representation_fees + contract.internet + contract.mobile + contract.mobile_plus + contract.transport_employer_cost) @api.depends('yearly_cost_before_charges', 'social_security_contributions', 'wage', 'social_security_contributions', 'double_holidays', 'warrants_cost', 'meal_voucher_paid_by_employer') def _compute_final_yearly_costs(self): for contract in self: contract.final_yearly_costs = ( contract.yearly_cost_before_charges + contract.social_security_contributions + contract.double_holidays + contract.warrants_cost + (220.0 * contract.meal_voucher_paid_by_employer)) @api.depends('holidays', 'final_yearly_costs') def _compute_holidays_compensation(self): for contract in self: if contract.holidays < 20: decrease_amount = contract.final_yearly_costs * ( 20.0 - contract.holidays) / 231.0 contract.holidays_compensation = decrease_amount else: contract.holidays_compensation = 0.0 @api.onchange('final_yearly_costs') def _onchange_final_yearly_costs(self): self.wage = self._get_gross_from_employer_costs( self.final_yearly_costs) @api.depends('meal_voucher_amount') def _compute_meal_voucher_paid_by_employer(self): for contract in self: contract.meal_voucher_paid_by_employer = contract.meal_voucher_amount * ( 1 - 0.1463) @api.depends('wage') def _compute_social_security_contributions(self): for contract in self: total_wage = contract.wage * 13.0 contract.social_security_contributions = (total_wage) * 0.3507 @api.depends('wage') def _compute_ucm_insurance(self): for contract in self: contract.ucm_insurance = (contract.wage * 12.0) * 0.05 @api.depends('public_transport_employee_amount') def _compute_public_transport_reimbursed_amount(self): for contract in self: contract.public_transport_reimbursed_amount = contract._get_public_transport_reimbursed_amount( contract.public_transport_employee_amount) def _get_public_transport_reimbursed_amount(self, amount): return amount * 0.68 @api.depends('final_yearly_costs') def _compute_monthly_yearly_costs(self): for contract in self: contract.monthly_yearly_costs = contract.final_yearly_costs / 12.0 @api.depends('wage') def _compute_holidays_advantages(self): for contract in self: contract.double_holidays = contract.wage * 0.92 contract.thirteen_month = contract.wage @api.onchange('transport_mode') def _onchange_transport_mode(self): if self.transport_mode != 'company_car': self.fuel_card = 0 self.company_car_total_depreciated_cost = 0 if self.transport_mode != 'others': self.others_reimbursed_amount = 0 if self.transport_mode != 'public_transports': self.public_transport_reimbursed_amount = 0 @api.onchange('mobile', 'mobile_plus') def _onchange_mobile(self): if self.mobile_plus and not self.mobile: raise ValidationError( _('You should have a mobile subscription to select an international communication amount!' )) def _get_internet_amount(self, has_internet): if has_internet: return self.get_attribute('internet', 'default_value') else: return 0.0 def _get_mobile_amount(self, has_mobile, international_communication): if has_mobile and international_communication: return self.get_attribute('mobile', 'default_value') + self.get_attribute( 'mobile_plus', 'default_value') elif has_mobile: return self.get_attribute('mobile', 'default_value') else: return 0.0 def _get_gross_from_employer_costs(self, yearly_cost): contract = self remaining_for_gross = yearly_cost \ - 12.0 * contract.representation_fees \ - 12.0 * contract.fuel_card \ - 12.0 * contract.internet \ - 12.0 * (contract.mobile + contract.mobile_plus) \ - 12.0 * contract.transport_employer_cost \ - contract.warrants_cost \ - 220.0 * contract.meal_voucher_paid_by_employer gross = remaining_for_gross / (13.0 + 13.0 * 0.3507 + 0.92) return gross
class WorkflowActionRule(models.Model): _name = "documents.workflow.rule" _description = "A set of condition and actions which will be available to all attachments matching the conditions" domain_folder_id = fields.Many2one('documents.folder', string="Folder", required=True, ondelete='cascade') name = fields.Char(required=True, string="Rule name", translate=True) note = fields.Char(string="Tooltip") # Conditions condition_type = fields.Selection([ ('criteria', "Criteria"), ('domain', "Domain"), ], default='criteria', string="Condition type") # Domain domain = fields.Char() # Criteria criteria_partner_id = fields.Many2one('res.partner', string="Contact") criteria_owner_id = fields.Many2one('res.users', string="Owner") criteria_tag_ids = fields.One2many('documents.workflow.tag.criteria', 'workflow_rule_id', string="Tags") # Actions partner_id = fields.Many2one('res.partner', string="Set Contact") user_id = fields.Many2one('res.users', string="Set Owner") tag_action_ids = fields.One2many('documents.workflow.action', 'workflow_rule_id', string='Set Tags') folder_id = fields.Many2one('documents.folder', string="Move to Folder") has_business_option = fields.Boolean(compute='_get_business') create_model = fields.Selection([], string="Create") # Activity remove_activities = fields.Boolean(string='Mark all Activities as done') activity_option = fields.Boolean(string='Create a new activity') activity_type_id = fields.Many2one('mail.activity.type', string="Activity type") activity_summary = fields.Char('Summary') activity_date_deadline_range = fields.Integer(string='Due Date In') activity_date_deadline_range_type = fields.Selection([ ('days', 'Days'), ('weeks', 'Weeks'), ('months', 'Months'), ], string='Due type', default='days') activity_note = fields.Html(string="Activity Note") activity_user_id = fields.Many2one('res.users', string='Responsible') @api.multi def _get_business(self): """ Checks if the workflow rule has available create models to display the option. """ for record in self: record.has_business_option = len(self._fields['create_model'].selection) def create_record(self, attachments=None): """ implemented by each link module to define specific fields for the new business model (create_values) :param attachments: the list of the attachments of the selection :return: the action dictionary that will be called after the workflow action is done or True. """ return True def apply_actions(self, attachment_ids): """ called by the front-end Document Inspector to apply the actions to the selection of ID's. :param context: attachment_ids[]: the list of attachments to apply the action. :return: if the action was to create a new business object, returns an action to open the view of the newly created object, else returns True. """ attachments = self.env['ir.attachment'].browse(attachment_ids) for attachment in attachments: # partner/owner/share_link/folder changes attachment_dict = {} if self.user_id: attachment_dict['owner_id'] = self.user_id.id if self.partner_id: attachment_dict['partner_id'] = self.partner_id.id if self.folder_id: attachment_dict['folder_id'] = self.folder_id.id attachment.write(attachment_dict) if self.remove_activities: attachment.activity_ids.action_feedback( feedback="completed by rule: %s. %s" % (self.name, self.note or '') ) if self.activity_option and self.activity_type_id: attachment.documents_set_activity(settings_model=self) # tag and facet actions for tag_action in self.tag_action_ids: tag_action.execute_action(attachment) if self.create_model: return self.create_record(attachments=attachments) return True
class LibraryBook(models.Model): _name = 'library.book' # this is the unique id for the model _description = 'Library Book' # this is a human readable name _inherit = ['base.archive'] _order = 'date_release desc, name' _rec_name = 'short_name' # Alternative field to use as name, used by osv’s name_get() (default: 'name') _sql_constraints = [('name_uniq', 'UNIQUE (name)', 'Book title must be unique.')] age_days = fields.Float( string='Days Since Release', compute='_compute_age', inverse='_inverse_age', search='_search_age', store=False, compute_sudo=False, ) author_ids = fields.Many2many('res.partner', string='Authors') cost_price = fields.Float('Book Cost', dp.get_precision('Book Price')) cover = fields.Binary('Book Cover') currency_id = fields.Many2one('res.currency', string='Currency') date_release = fields.Date('Release Date') date_updated = fields.Datetime('Last Updated') description = fields.Html( string='Description', # optional: sanitize=True, strip_style=False, translate=False, ) isbn = fields.Char('ISBN') manager_remarks = fields.Text('Manager Remarks') name = fields.Char('Title', required=True) notes = fields.Text('Internal Notes') out_of_print = fields.Boolean('Out of Print?') pages = fields.Integer( string='Number of Pages', default=0, help='Total book pages count', groups='base.group_user', states={'cancel': [('readonly', True)]}, copy=True, index=False, readonly=False, required=False, company_dependent=False, ) publisher_id = fields.Many2one( 'res.partner', string='Publisher', # optional ondelete='set null', context={}, domain=[], ) publisher_city = fields.Char('Publisher City', related='publisher_id.city') reader_rating = fields.Float( 'Reader Average Rating', (14, 4), # Optional precision (total, decimals), ) ref_doc_id = fields.Reference(selection='_referencable_models', string='Reference Document') retail_price = fields.Monetary( 'Retail Price', currency_field='currency_id', # Optional ) short_name = fields.Char( string='Short Title', size=100, # For Char only translate=False, # also for Text fields ) state = fields.Selection([('draft', 'Not Available'), ('available', 'Available'), ('lost', 'Lost')], 'State') @api.multi def name_get(self): result = [] for book in self: authors = book.author_ids.mapped('name') name = u'%s (%s)' % (book.title, u', '.join(authors)) result.append((book.id, name)) return result @api.constrains('date_release') def _check_release_date(self): for r in self: if r.date_release > fields.Date.today(): raise models.ValidationError( 'Release date must be in the past') @api.depends def _compute_age(self): today = fDate.from_string(fDate.today()) for book in self.filtered('date_release'): delta = (fDate.from_string(book.date_release - today)) book.age_days = delta.days def _inverse_age(self): today = fDate.from_string(fDate.today()) for book in self.filtered('date_release'): d = td(days=book.age_days) - today book.date_release = fDate.to_string(d) def _search_age(self, operator, value): today = fDate.from_string(fDate.today()) value_days = td(days=value) value_date = fDate.to_string(today - value_days) return [('date_release', operator, value_date)] @api.model def _referencable_models(self): models = self.env['res.request.link'].search([]) return [(x.object, x.name) for x in models] @api.model def is_allowed_transition(self, old_state, new_state): allowed = [('draft', 'available'), ('available', 'borrowed'), ('borrowed', 'available'), ('available', 'lost'), ('lost', 'available')] return (old_state, new_state) in allowed @api.multi def change_state(self, new_state): for book in self: if book.is_allowed_transition(book.state, new_state): book.state = new_state else: continue @api.model @api.returns('self', lambda rec: rec.id) def create(self, values): if not self.user_has_groups('library.group_library_manager'): if 'manager_remarks' in values: raise exceptions.UserError('You are not allowed to modify', 'manager_remarks') return super(LibraryBook, self).create(values) @api.multi def write(self, values): if not self.user_has_groups('library.group_library_manager'): if 'manager_remarks' in values: raise exceptions.UserError('You are not allowed to modify', 'manager_remarks') return super(LibraryBook, self).write(values) @api.model def fields_get(self, allfields=None, write_access=True, attributes=None): fields = super(LibraryBook, self).fields_get(allfields=allfields, write_access=write_access, attributes=attributes) if not self.user_has_groups('library.group_library_manager'): if 'manager_remarks' in fields: fields['manager_remarks']['readonly'] = True @api.model def _name_search(self, name='', args=None, operator='ilike', limit=100, name_get_uid=None): args = [] if args is None else args.copy() if not (name == '' and operator == 'ilike'): args += [ '|', '|', ('name', operator, name), ('isbn', operator, name), ('author_ids.name', operator, name) ] return super(LibraryBook, self)._name_search(name='', args=args, operator='ilike', limit=limit, name_get_uid=name_get_uid)
class HrEmployee(models.Model): _inherit = 'hr.employee' spouse_fiscal_status = fields.Selection( [('without income', 'Without Income'), ('with income', 'With Income')], string='Tax status for spouse', groups="hr.group_hr_user") disabled = fields.Boolean( string="Disabled", help="If the employee is declared disabled by law", groups="hr.group_hr_user") disabled_spouse_bool = fields.Boolean( string='Disabled Spouse', help='if recipient spouse is declared disabled by law', groups="hr.group_hr_user") disabled_children_bool = fields.Boolean( string='Disabled Children', help='if recipient children is/are declared disabled by law', groups="hr.group_hr_user") resident_bool = fields.Boolean( string='Nonresident', help='if recipient lives in a foreign country', groups="hr.group_hr_user") disabled_children_number = fields.Integer('Number of disabled children', groups="hr.group_hr_user") dependent_children = fields.Integer( compute='_compute_dependent_children', string='Considered number of dependent children', groups="hr.group_hr_user") other_dependent_people = fields.Boolean( string="Other Dependent People", help="If other people are dependent on the employee", groups="hr.group_hr_user") other_senior_dependent = fields.Integer( '# seniors (>=65)', help= "Number of seniors dependent on the employee, including the disabled ones", groups="hr.group_hr_user") other_disabled_senior_dependent = fields.Integer( '# disabled seniors (>=65)', groups="hr.group_hr_user") other_juniors_dependent = fields.Integer( '# people (<65)', help= "Number of juniors dependent on the employee, including the disabled ones", groups="hr.group_hr_user") other_disabled_juniors_dependent = fields.Integer( '# disabled people (<65)', groups="hr.group_hr_user") dependent_seniors = fields.Integer( compute='_compute_dependent_people', string="Considered number of dependent seniors", groups="hr.group_hr_user") dependent_juniors = fields.Integer( compute='_compute_dependent_people', string="Considered number of dependent juniors", groups="hr.group_hr_user") spouse_net_revenue = fields.Float( string="Spouse Net Revenue", help= "Own professional income, other than pensions, annuities or similar income", groups="hr.group_hr_user") spouse_other_net_revenue = fields.Float( string="Spouse Other Net Revenue", help= 'Own professional income which is exclusively composed of pensions, annuities or similar income', groups="hr.group_hr_user") @api.constrains('spouse_fiscal_status', 'spouse_net_revenue', 'spouse_other_net_revenue') def _check_spouse_revenue(self): for employee in self: if employee.spouse_fiscal_status == 'with income' and not employee.spouse_net_revenue and not employee.spouse_other_net_revenue: raise ValidationError( _("The revenue for the spouse can't be equal to zero is the fiscal status is 'With Income'." )) @api.onchange('spouse_fiscal_status') def _onchange_spouse_fiscal_status(self): self.spouse_net_revenue = 0.0 self.spouse_other_net_revenue = 0.0 @api.onchange('disabled_children_bool') def _onchange_disabled_children_bool(self): self.disabled_children_number = 0 @api.onchange('other_dependent_people') def _onchange_other_dependent_people(self): self.other_senior_dependent = 0.0 self.other_disabled_senior_dependent = 0.0 self.other_juniors_dependent = 0.0 self.other_disabled_juniors_dependent = 0.0 @api.depends('disabled_children_bool', 'disabled_children_number', 'children') def _compute_dependent_children(self): for employee in self: if employee.disabled_children_bool: employee.dependent_children = employee.children + employee.disabled_children_number else: employee.dependent_children = employee.children @api.depends('other_dependent_people', 'other_senior_dependent', 'other_disabled_senior_dependent', 'other_juniors_dependent', 'other_disabled_juniors_dependent') def _compute_dependent_people(self): for employee in self: employee.dependent_seniors = employee.other_senior_dependent + employee.other_disabled_senior_dependent employee.dependent_juniors = employee.other_juniors_dependent + employee.other_disabled_juniors_dependent
class PaymentAcquirer(models.Model): _inherit = 'payment.acquirer' provider = fields.Selection( selection_add=[('payulatam', 'PayU Latam')], ondelete={'payulatam': 'set default'}) payulatam_merchant_id = fields.Char( string="PayU Latam Merchant ID", help="The ID solely used to identify the account with PayULatam", required_if_provider='payulatam') payulatam_account_id = fields.Char( string="PayU Latam Account ID", help="The ID solely used to identify the country-dependent shop with PayULatam", required_if_provider='payulatam') payulatam_api_key = fields.Char( string="PayU Latam API Key", required_if_provider='payulatam', groups='base.group_system') @api.model def _get_compatible_acquirers(self, *args, currency_id=None, **kwargs): """ Override of payment to unlist PayU Latam acquirers for unsupported currencies. """ acquirers = super()._get_compatible_acquirers(*args, currency_id=currency_id, **kwargs) currency = self.env['res.currency'].browse(currency_id).exists() if currency and currency.name not in SUPPORTED_CURRENCIES: acquirers = acquirers.filtered(lambda a: a.provider != 'payulatam') return acquirers def _payulatam_generate_sign(self, values, incoming=True): """ Generate the signature for incoming or outgoing communications. :param dict values: The values used to generate the signature :param bool incoming: Whether the signature must be generated for an incoming (PayU Latam to Odoo) or outgoing (Odoo to PayU Latam) communication. :return: The signature :rtype: str """ if incoming: data_string = '~'.join([ self.payulatam_api_key, self.payulatam_merchant_id, values['referenceCode'], # http://developers.payulatam.com/en/web_checkout/integration.html # Section: 2. Response page > Signature validation # PayU Latam use the "Round half to even" rounding method # to generate their signature. This happens to be Python 3's # default rounding method. float_repr(float(values.get('TX_VALUE')), 1), values['currency'], values.get('transactionState'), ]) else: data_string = '~'.join([ self.payulatam_api_key, self.payulatam_merchant_id, values['referenceCode'], float_repr(float(values['amount']), 1), values['currency'], ]) return md5(data_string.encode('utf-8')).hexdigest() def _get_default_payment_method_id(self): self.ensure_one() if self.provider != 'payulatam': return super()._get_default_payment_method_id() return self.env.ref('payment_payulatam.payment_method_payulatam').id def _neutralize(self): super()._neutralize() self._neutralize_fields('payulatam', [ 'payulatam_merchant_id', 'payulatam_account_id', 'payulatam_api_key', ])
class Slide(models.Model): _name = 'slide.slide' _inherit = [ 'mail.thread', 'image.mixin', 'website.seo.metadata', 'website.published.mixin' ] _description = 'Slides' _mail_post_access = 'read' _order_by_strategy = { 'sequence': 'sequence asc', 'most_viewed': 'total_views desc', 'most_voted': 'likes desc', 'latest': 'date_published desc', } _order = 'sequence asc, is_category asc' # description name = fields.Char('Title', required=True, translate=True) active = fields.Boolean(default=True, tracking=100) sequence = fields.Integer('Sequence', default=0) user_id = fields.Many2one('res.users', string='Uploaded by', default=lambda self: self.env.uid) description = fields.Text('Description', translate=True) channel_id = fields.Many2one('slide.channel', string="Course", required=True) tag_ids = fields.Many2many('slide.tag', 'rel_slide_tag', 'slide_id', 'tag_id', string='Tags') is_preview = fields.Boolean( 'Allow Preview', default=False, help= "The course is accessible by anyone : the users don't need to join the channel to access the content of the course." ) is_new_slide = fields.Boolean('Is New Slide', compute='_compute_is_new_slide') completion_time = fields.Float( 'Duration', digits=(10, 4), help="The estimated completion time for this slide") # Categories is_category = fields.Boolean('Is a category', default=False) category_id = fields.Many2one('slide.slide', string="Section", compute="_compute_category_id", store=True) slide_ids = fields.One2many('slide.slide', "category_id", string="Slides") # subscribers partner_ids = fields.Many2many( 'res.partner', 'slide_slide_partner', 'slide_id', 'partner_id', string='Subscribers', groups='website_slides.group_website_slides_officer', copy=False) slide_partner_ids = fields.One2many( 'slide.slide.partner', 'slide_id', string='Subscribers information', groups='website_slides.group_website_slides_officer', copy=False) user_membership_id = fields.Many2one( 'slide.slide.partner', string="Subscriber information", compute='_compute_user_membership_id', compute_sudo=False, help="Subscriber information for the current logged in user") # Quiz related fields question_ids = fields.One2many("slide.question", "slide_id", string="Questions") questions_count = fields.Integer(string="Numbers of Questions", compute='_compute_questions_count') quiz_first_attempt_reward = fields.Integer("Reward: first attempt", default=10) quiz_second_attempt_reward = fields.Integer("Reward: second attempt", default=7) quiz_third_attempt_reward = fields.Integer( "Reward: third attempt", default=5, ) quiz_fourth_attempt_reward = fields.Integer( "Reward: every attempt after the third try", default=2) # content slide_type = fields.Selection( [('infographic', 'Infographic'), ('webpage', 'Web Page'), ('presentation', 'Presentation'), ('document', 'Document'), ('video', 'Video'), ('quiz', "Quiz")], string='Type', required=True, default='document', help= "The document type will be set automatically based on the document URL and properties (e.g. height and width for presentation and document)." ) datas = fields.Binary('Content', attachment=True) url = fields.Char('Document URL', help="Youtube or Google Document URL") document_id = fields.Char('Document ID', help="Youtube or Google Document ID") link_ids = fields.One2many('slide.slide.link', 'slide_id', string="External URL for this slide") slide_resource_ids = fields.One2many( 'slide.slide.resource', 'slide_id', string="Additional Resource for this slide") slide_resource_downloadable = fields.Boolean( 'Allow Download', default=False, help="Allow the user to download the content of the slide.") mime_type = fields.Char('Mime-type') html_content = fields.Html( "HTML Content", help="Custom HTML content for slides of type 'Web Page'.", translate=True, sanitize_form=False) # website website_id = fields.Many2one(related='channel_id.website_id', readonly=True) date_published = fields.Datetime('Publish Date', readonly=True, tracking=1) likes = fields.Integer('Likes', compute='_compute_user_info', store=True, compute_sudo=False) dislikes = fields.Integer('Dislikes', compute='_compute_user_info', store=True, compute_sudo=False) user_vote = fields.Integer('User vote', compute='_compute_user_info', compute_sudo=False) embed_code = fields.Text('Embed Code', readonly=True, compute='_compute_embed_code') # views embedcount_ids = fields.One2many('slide.embed', 'slide_id', string="Embed Count") slide_views = fields.Integer('# of Website Views', store=True, compute="_compute_slide_views") public_views = fields.Integer('# of Public Views', copy=False) total_views = fields.Integer("Views", default="0", compute='_compute_total', store=True) # comments comments_count = fields.Integer('Number of comments', compute="_compute_comments_count") # channel channel_type = fields.Selection(related="channel_id.channel_type", string="Channel type") channel_allow_comment = fields.Boolean(related="channel_id.allow_comment", string="Allows comment") # Statistics in case the slide is a category nbr_presentation = fields.Integer("Number of Presentations", compute='_compute_slides_statistics', store=True) nbr_document = fields.Integer("Number of Documents", compute='_compute_slides_statistics', store=True) nbr_video = fields.Integer("Number of Videos", compute='_compute_slides_statistics', store=True) nbr_infographic = fields.Integer("Number of Infographics", compute='_compute_slides_statistics', store=True) nbr_webpage = fields.Integer("Number of Webpages", compute='_compute_slides_statistics', store=True) nbr_quiz = fields.Integer("Number of Quizs", compute="_compute_slides_statistics", store=True) total_slides = fields.Integer(compute='_compute_slides_statistics', store=True) _sql_constraints = [( 'exclusion_html_content_and_url', "CHECK(html_content IS NULL OR url IS NULL)", "A slide is either filled with a document url or HTML content. Not both." )] @api.depends('date_published', 'is_published') def _compute_is_new_slide(self): for slide in self: slide.is_new_slide = slide.date_published > fields.Datetime.now( ) - relativedelta(days=7) if slide.is_published else False @api.depends('channel_id.slide_ids.is_category', 'channel_id.slide_ids.sequence') def _compute_category_id(self): """ Will take all the slides of the channel for which the index is higher than the index of this category and lower than the index of the next category. Lists are manually sorted because when adding a new browse record order will not be correct as the added slide would actually end up at the first place no matter its sequence.""" self.category_id = False # initialize whatever the state channel_slides = {} for slide in self: if slide.channel_id.id not in channel_slides: channel_slides[ slide.channel_id.id] = slide.channel_id.slide_ids for cid, slides in channel_slides.items(): current_category = self.env['slide.slide'] slide_list = list(slides) slide_list.sort(key=lambda s: (s.sequence, not s.is_category)) for slide in slide_list: if slide.is_category: current_category = slide elif slide.category_id != current_category: slide.category_id = current_category.id @api.depends('question_ids') def _compute_questions_count(self): for slide in self: slide.questions_count = len(slide.question_ids) @api.depends('website_message_ids.res_id', 'website_message_ids.model', 'website_message_ids.message_type') def _compute_comments_count(self): for slide in self: slide.comments_count = len(slide.website_message_ids) @api.depends('slide_views', 'public_views') def _compute_total(self): for record in self: record.total_views = record.slide_views + record.public_views @api.depends('slide_partner_ids.vote') @api.depends_context('uid') def _compute_user_info(self): slide_data = dict.fromkeys( self.ids, dict({ 'likes': 0, 'dislikes': 0, 'user_vote': False })) slide_partners = self.env['slide.slide.partner'].sudo().search([ ('slide_id', 'in', self.ids) ]) for slide_partner in slide_partners: if slide_partner.vote == 1: slide_data[slide_partner.slide_id.id]['likes'] += 1 if slide_partner.partner_id == self.env.user.partner_id: slide_data[slide_partner.slide_id.id]['user_vote'] = 1 elif slide_partner.vote == -1: slide_data[slide_partner.slide_id.id]['dislikes'] += 1 if slide_partner.partner_id == self.env.user.partner_id: slide_data[slide_partner.slide_id.id]['user_vote'] = -1 for slide in self: slide.update(slide_data[slide.id]) @api.depends('slide_partner_ids.slide_id') def _compute_slide_views(self): # TODO awa: tried compute_sudo, for some reason it doesn't work in here... read_group_res = self.env['slide.slide.partner'].sudo().read_group( [('slide_id', 'in', self.ids)], ['slide_id'], groupby=['slide_id']) mapped_data = dict((res['slide_id'][0], res['slide_id_count']) for res in read_group_res) for slide in self: slide.slide_views = mapped_data.get(slide.id, 0) @api.depends('slide_ids.sequence', 'slide_ids.slide_type', 'slide_ids.is_published', 'slide_ids.is_category') def _compute_slides_statistics(self): # Do not use dict.fromkeys(self.ids, dict()) otherwise it will use the same dictionnary for all keys. # Therefore, when updating the dict of one key, it updates the dict of all keys. keys = [ 'nbr_%s' % slide_type for slide_type in self.env['slide.slide']._fields['slide_type'].get_values(self.env) ] default_vals = dict((key, 0) for key in keys + ['total_slides']) res = self.env['slide.slide'].read_group( [('is_published', '=', True), ('category_id', 'in', self.ids), ('is_category', '=', False)], ['category_id', 'slide_type'], ['category_id', 'slide_type'], lazy=False) type_stats = self._compute_slides_statistics_type(res) for record in self: record.update(type_stats.get(record._origin.id, default_vals)) def _compute_slides_statistics_type(self, read_group_res): """ Compute statistics based on all existing slide types """ slide_types = self.env['slide.slide']._fields['slide_type'].get_values( self.env) keys = ['nbr_%s' % slide_type for slide_type in slide_types] result = dict((cid, dict((key, 0) for key in keys + ['total_slides'])) for cid in self.ids) for res_group in read_group_res: cid = res_group['category_id'][0] slide_type = res_group.get('slide_type') if slide_type: slide_type_count = res_group.get('__count', 0) result[cid]['nbr_%s' % slide_type] = slide_type_count result[cid]['total_slides'] += slide_type_count return result @api.depends('slide_partner_ids.partner_id') @api.depends('uid') def _compute_user_membership_id(self): slide_partners = self.env['slide.slide.partner'].sudo().search([ ('slide_id', 'in', self.ids), ('partner_id', '=', self.env.user.partner_id.id), ]) for record in self: record.user_membership_id = next( (slide_partner for slide_partner in slide_partners if slide_partner.slide_id == record), self.env['slide.slide.partner']) @api.depends('document_id', 'slide_type', 'mime_type') def _compute_embed_code(self): base_url = request and request.httprequest.url_root or self.env[ 'ir.config_parameter'].sudo().get_param('web.base.url') if base_url[-1] == '/': base_url = base_url[:-1] for record in self: if record.datas and (not record.document_id or record.slide_type in ['document', 'presentation']): slide_url = base_url + url_for( '/slides/embed/%s?page=1' % record.id) record.embed_code = '<iframe src="%s" class="o_wslides_iframe_viewer" allowFullScreen="true" height="%s" width="%s" frameborder="0"></iframe>' % ( slide_url, 315, 420) elif record.slide_type == 'video' and record.document_id: if not record.mime_type: # embed youtube video record.embed_code = '<iframe src="//www.youtube.com/embed/%s?theme=light" allowFullScreen="true" frameborder="0"></iframe>' % ( record.document_id) else: # embed google doc video record.embed_code = '<iframe src="//drive.google.com/file/d/%s/preview" allowFullScreen="true" frameborder="0"></iframe>' % ( record.document_id) else: record.embed_code = False @api.onchange('url') def _on_change_url(self): self.ensure_one() if self.url: res = self._parse_document_url(self.url) if res.get('error'): raise Warning(res.get('error')) values = res['values'] if not values.get('document_id'): raise Warning( _('Please enter valid Youtube or Google Doc URL')) for key, value in values.items(): self[key] = value @api.onchange('datas') def _on_change_datas(self): """ For PDFs, we assume that it takes 5 minutes to read a page. If the selected file is not a PDF, it is an image (You can only upload PDF or Image file) then the slide_type is changed into infographic and the uploaded dataS is transfered to the image field. (It avoids the infinite loading in PDF viewer)""" if self.datas: data = base64.b64decode(self.datas) if data.startswith(b'%PDF-'): pdf = PyPDF2.PdfFileReader(io.BytesIO(data), overwriteWarnings=False) self.completion_time = (5 * len(pdf.pages)) / 60 else: self.slide_type = 'infographic' self.image_1920 = self.datas self.datas = None @api.depends('name', 'channel_id.website_id.domain') def _compute_website_url(self): # TDE FIXME: clena this link.tracker strange stuff super(Slide, self)._compute_website_url() for slide in self: if slide.id: # avoid to perform a slug on a not yet saved record in case of an onchange. base_url = slide.channel_id.get_base_url() # link_tracker is not in dependencies, so use it to shorten url only if installed. if self.env.registry.get('link.tracker'): url = self.env['link.tracker'].sudo().create({ 'url': '%s/slides/slide/%s' % (base_url, slug(slide)), 'title': slide.name, }).short_url else: url = '%s/slides/slide/%s' % (base_url, slug(slide)) slide.website_url = url @api.depends('channel_id.can_publish') def _compute_can_publish(self): for record in self: record.can_publish = record.channel_id.can_publish @api.model def _get_can_publish_error_message(self): return _( "Publishing is restricted to the responsible of training courses or members of the publisher group for documentation courses" ) # --------------------------------------------------------- # ORM Overrides # --------------------------------------------------------- @api.model def create(self, values): # Do not publish slide if user has not publisher rights channel = self.env['slide.channel'].browse(values['channel_id']) if not channel.can_publish: # 'website_published' is handled by mixin values['date_published'] = False if values.get('slide_type' ) == 'infographic' and not values.get('image_1920'): values['image_1920'] = values['datas'] if values.get('is_category'): values['is_preview'] = True values['is_published'] = True if values.get('is_published') and not values.get('date_published'): values['date_published'] = datetime.datetime.now() if values.get('url') and not values.get('document_id'): doc_data = self._parse_document_url(values['url']).get( 'values', dict()) for key, value in doc_data.items(): values.setdefault(key, value) slide = super(Slide, self).create(values) if slide.is_published and not slide.is_category: slide._post_publication() return slide def write(self, values): if values.get('url') and values['url'] != self.url: doc_data = self._parse_document_url(values['url']).get( 'values', dict()) for key, value in doc_data.items(): values.setdefault(key, value) if values.get('is_category'): values['is_preview'] = True values['is_published'] = True res = super(Slide, self).write(values) if values.get('is_published'): self.date_published = datetime.datetime.now() self._post_publication() if 'is_published' in values or 'active' in values: # if the slide is published/unpublished, recompute the completion for the partners self.slide_partner_ids._set_completed_callback() return res @api.returns('self', lambda value: value.id) def copy(self, default=None): """Sets the sequence to zero so that it always lands at the beginning of the newly selected course as an uncategorized slide""" rec = super(Slide, self).copy(default) rec.sequence = 0 return rec def unlink(self): if self.question_ids and self.channel_id.channel_partner_ids: raise UserError( _("People already took this quiz. To keep course progression it should not be deleted." )) super(Slide, self).unlink() # --------------------------------------------------------- # Mail/Rating # --------------------------------------------------------- @api.returns('mail.message', lambda value: value.id) def message_post(self, *, message_type='notification', **kwargs): self.ensure_one() if message_type == 'comment' and not self.channel_id.can_comment: # user comments have a restriction on karma raise AccessError(_('Not enough karma to comment')) return super(Slide, self).message_post(message_type=message_type, **kwargs) def get_access_action(self, access_uid=None): """ Instead of the classic form view, redirect to website if it is published. """ self.ensure_one() if self.website_published: return { 'type': 'ir.actions.act_url', 'url': '%s' % self.website_url, 'target': 'self', 'target_type': 'public', 'res_id': self.id, } return super(Slide, self).get_access_action(access_uid) def _notify_get_groups(self): """ Add access button to everyone if the document is active. """ groups = super(Slide, self)._notify_get_groups() if self.website_published: for group_name, group_method, group_data in groups: group_data['has_button_access'] = True return groups # --------------------------------------------------------- # Business Methods # --------------------------------------------------------- def _post_publication(self): base_url = self.env['ir.config_parameter'].sudo().get_param( 'web.base.url') for slide in self.filtered(lambda slide: slide.website_published and slide.channel_id.publish_template_id): publish_template = slide.channel_id.publish_template_id html_body = publish_template.with_context( base_url=base_url)._render_field('body_html', slide.ids)[slide.id] subject = publish_template._render_field('subject', slide.ids)[slide.id] slide.channel_id.with_context( mail_create_nosubscribe=True).message_post( subject=subject, body=html_body, subtype_xmlid='website_slides.mt_channel_slide_published', email_layout_xmlid='mail.mail_notification_light', ) return True def _generate_signed_token(self, partner_id): """ Lazy generate the acces_token and return it signed by the given partner_id :rtype tuple (string, int) :return (signed_token, partner_id) """ if not self.access_token: self.write({'access_token': self._default_access_token()}) return self._sign_token(partner_id) def _send_share_email(self, email, fullscreen): # TDE FIXME: template to check mail_ids = [] base_url = self.env['ir.config_parameter'].sudo().get_param( 'web.base.url') for record in self: template = self.channel_id.share_template_id.with_context( user=self.env.user, email=email, base_url=base_url, fullscreen=fullscreen) email_values = {'email_to': email} if self.env.user.has_group('base.group_portal'): template = template.sudo() email_values[ 'email_from'] = self.env.company.catchall_formatted or self.env.company.email_formatted mail_ids.append( template.send_mail(record.id, notif_layout='mail.mail_notification_light', email_values=email_values)) return mail_ids def action_like(self): self.check_access_rights('read') self.check_access_rule('read') return self._action_vote(upvote=True) def action_dislike(self): self.check_access_rights('read') self.check_access_rule('read') return self._action_vote(upvote=False) def _action_vote(self, upvote=True): """ Private implementation of voting. It does not check for any real access rights; public methods should grant access before calling this method. :param upvote: if True, is a like; if False, is a dislike """ self_sudo = self.sudo() SlidePartnerSudo = self.env['slide.slide.partner'].sudo() slide_partners = SlidePartnerSudo.search([ ('slide_id', 'in', self.ids), ('partner_id', '=', self.env.user.partner_id.id) ]) slide_id = slide_partners.mapped('slide_id') new_slides = self_sudo - slide_id channel = slide_id.channel_id karma_to_add = 0 for slide_partner in slide_partners: if upvote: new_vote = 0 if slide_partner.vote == -1 else 1 if slide_partner.vote != 1: karma_to_add += channel.karma_gen_slide_vote else: new_vote = 0 if slide_partner.vote == 1 else -1 if slide_partner.vote != -1: karma_to_add -= channel.karma_gen_slide_vote slide_partner.vote = new_vote for new_slide in new_slides: new_vote = 1 if upvote else -1 new_slide.write({ 'slide_partner_ids': [(0, 0, { 'vote': new_vote, 'partner_id': self.env.user.partner_id.id })] }) karma_to_add += new_slide.channel_id.karma_gen_slide_vote * ( 1 if upvote else -1) if karma_to_add: self.env.user.add_karma(karma_to_add) def action_set_viewed(self, quiz_attempts_inc=False): if not all(slide.channel_id.is_member for slide in self): raise UserError( _('You cannot mark a slide as viewed if you are not among its members.' )) return bool( self._action_set_viewed(self.env.user.partner_id, quiz_attempts_inc=quiz_attempts_inc)) def _action_set_viewed(self, target_partner, quiz_attempts_inc=False): self_sudo = self.sudo() SlidePartnerSudo = self.env['slide.slide.partner'].sudo() existing_sudo = SlidePartnerSudo.search([('slide_id', 'in', self.ids), ('partner_id', '=', target_partner.id)]) if quiz_attempts_inc: for exsting_slide in existing_sudo: exsting_slide.write({ 'quiz_attempts_count': exsting_slide.quiz_attempts_count + 1 }) new_slides = self_sudo - existing_sudo.mapped('slide_id') return SlidePartnerSudo.create([{ 'slide_id': new_slide.id, 'channel_id': new_slide.channel_id.id, 'partner_id': target_partner.id, 'quiz_attempts_count': 1 if quiz_attempts_inc else 0, 'vote': 0 } for new_slide in new_slides]) def action_set_completed(self): if not all(slide.channel_id.is_member for slide in self): raise UserError( _('You cannot mark a slide as completed if you are not among its members.' )) return self._action_set_completed(self.env.user.partner_id) def _action_set_completed(self, target_partner): self_sudo = self.sudo() SlidePartnerSudo = self.env['slide.slide.partner'].sudo() existing_sudo = SlidePartnerSudo.search([('slide_id', 'in', self.ids), ('partner_id', '=', target_partner.id)]) existing_sudo.write({'completed': True}) new_slides = self_sudo - existing_sudo.mapped('slide_id') SlidePartnerSudo.create([{ 'slide_id': new_slide.id, 'channel_id': new_slide.channel_id.id, 'partner_id': target_partner.id, 'vote': 0, 'completed': True } for new_slide in new_slides]) return True def _action_set_quiz_done(self): if not all(slide.channel_id.is_member for slide in self): raise UserError( _('You cannot mark a slide quiz as completed if you are not among its members.' )) points = 0 for slide in self: user_membership_sudo = slide.user_membership_id.sudo() if not user_membership_sudo or user_membership_sudo.completed or not user_membership_sudo.quiz_attempts_count: continue gains = [ slide.quiz_first_attempt_reward, slide.quiz_second_attempt_reward, slide.quiz_third_attempt_reward, slide.quiz_fourth_attempt_reward ] points += gains[ user_membership_sudo.quiz_attempts_count - 1] if user_membership_sudo.quiz_attempts_count <= len( gains) else gains[-1] return self.env.user.sudo().add_karma(points) def _compute_quiz_info(self, target_partner, quiz_done=False): result = dict.fromkeys(self.ids, False) slide_partners = self.env['slide.slide.partner'].sudo().search([ ('slide_id', 'in', self.ids), ('partner_id', '=', target_partner.id) ]) slide_partners_map = dict( (sp.slide_id.id, sp) for sp in slide_partners) for slide in self: if not slide.question_ids: gains = [0] else: gains = [ slide.quiz_first_attempt_reward, slide.quiz_second_attempt_reward, slide.quiz_third_attempt_reward, slide.quiz_fourth_attempt_reward ] result[slide.id] = { 'quiz_karma_max': gains[0], # what could be gained if succeed at first try 'quiz_karma_gain': gains[0], # what would be gained at next test 'quiz_karma_won': 0, # what has been gained 'quiz_attempts_count': 0, # number of attempts } slide_partner = slide_partners_map.get(slide.id) if slide.question_ids and slide_partner: if slide_partner.quiz_attempts_count: result[slide.id]['quiz_karma_gain'] = gains[ slide_partner. quiz_attempts_count] if slide_partner.quiz_attempts_count < len( gains) else gains[-1] result[slide.id][ 'quiz_attempts_count'] = slide_partner.quiz_attempts_count if quiz_done or slide_partner.completed: result[slide.id]['quiz_karma_won'] = gains[ slide_partner.quiz_attempts_count - 1] if slide_partner.quiz_attempts_count < len( gains) else gains[-1] return result # -------------------------------------------------- # Parsing methods # -------------------------------------------------- @api.model def _fetch_data(self, base_url, params, content_type=False): result = {'values': dict()} try: response = requests.get(base_url, timeout=3, params=params) response.raise_for_status() if content_type == 'json': result['values'] = response.json() elif content_type in ('image', 'pdf'): result['values'] = base64.b64encode(response.content) else: result['values'] = response.content except requests.exceptions.HTTPError as e: result['error'] = e.response.content except requests.exceptions.ConnectionError as e: result['error'] = str(e) return result def _find_document_data_from_url(self, url): url_obj = urls.url_parse(url) if url_obj.ascii_host == 'youtu.be': return ('youtube', url_obj.path[1:] if url_obj.path else False) elif url_obj.ascii_host in ('youtube.com', 'www.youtube.com', 'm.youtube.com'): v_query_value = url_obj.decode_query().get('v') if v_query_value: return ('youtube', v_query_value) split_path = url_obj.path.split('/') if len(split_path) >= 3 and split_path[1] in ('v', 'embed'): return ('youtube', split_path[2]) expr = re.compile( r'(^https:\/\/docs.google.com|^https:\/\/drive.google.com).*\/d\/([^\/]*)' ) arg = expr.match(url) document_id = arg and arg.group(2) or False if document_id: return ('google', document_id) return (None, False) def _parse_document_url(self, url, only_preview_fields=False): document_source, document_id = self._find_document_data_from_url(url) if document_source and hasattr(self, '_parse_%s_document' % document_source): return getattr(self, '_parse_%s_document' % document_source)( document_id, only_preview_fields) return {'error': _('Unknown document')} def _parse_youtube_document(self, document_id, only_preview_fields): """ If we receive a duration (YT video), we use it to determine the slide duration. The received duration is under a special format (e.g: PT1M21S15, meaning 1h 21m 15s). """ key = self.env['website'].get_current_website( ).website_slide_google_app_key fetch_res = self._fetch_data( 'https://www.googleapis.com/youtube/v3/videos', { 'id': document_id, 'key': key, 'part': 'snippet,contentDetails', 'fields': 'items(id,snippet,contentDetails)' }, 'json') if fetch_res.get('error'): return { 'error': self._extract_google_error_message(fetch_res.get('error')) } values = {'slide_type': 'video', 'document_id': document_id} items = fetch_res['values'].get('items') if not items: return {'error': _('Please enter valid Youtube or Google Doc URL')} youtube_values = items[0] youtube_duration = youtube_values.get('contentDetails', {}).get('duration') if youtube_duration: parsed_duration = re.search( r'^PT(?:(\d+)H)?(?:(\d+)M)?(?:(\d+)S)?$', youtube_duration) values['completion_time'] = (int(parsed_duration.group(1) or 0)) + \ (int(parsed_duration.group(2) or 0) / 60) + \ (int(parsed_duration.group(3) or 0) / 3600) if youtube_values.get('snippet'): snippet = youtube_values['snippet'] if only_preview_fields: values.update({ 'url_src': snippet['thumbnails']['high']['url'], 'title': snippet['title'], 'description': snippet['description'] }) return values values.update({ 'name': snippet['title'], 'image_1920': self._fetch_data(snippet['thumbnails']['high']['url'], {}, 'image')['values'], 'description': snippet['description'], 'mime_type': False, }) return {'values': values} def _extract_google_error_message(self, error): """ See here for Google error format https://developers.google.com/drive/api/v3/handle-errors """ try: error = json.loads(error) error = (error.get('error', {}).get('errors', []) or [{}])[0].get('reason') except json.decoder.JSONDecodeError: error = str(error) if error == 'keyInvalid': return _( 'Your Google API key is invalid, please update it into your settings.\nSettings > Website > Features > API Key' ) return _( 'Could not fetch data from url. Document or access right not available:\n%s' ) % error @api.model def _parse_google_document(self, document_id, only_preview_fields): def get_slide_type(vals): # TDE FIXME: WTF ?? slide_type = 'presentation' if vals.get('image_1920'): image = Image.open( io.BytesIO(base64.b64decode(vals['image_1920']))) width, height = image.size if height > width: return 'document' return slide_type # Google drive doesn't use a simple API key to access the data, but requires an access # token. However, this token is generated in module google_drive, which is not in the # dependencies of website_slides. We still keep the 'key' parameter just in case, but that # is probably useless. params = {} params['projection'] = 'BASIC' if 'google.drive.config' in self.env: access_token = self.env['google.drive.config'].get_access_token() if access_token: params['access_token'] = access_token if not params.get('access_token'): params['key'] = self.env['website'].get_current_website( ).website_slide_google_app_key fetch_res = self._fetch_data( 'https://www.googleapis.com/drive/v2/files/%s' % document_id, params, "json") if fetch_res.get('error'): return { 'error': self._extract_google_error_message(fetch_res.get('error')) } google_values = fetch_res['values'] if only_preview_fields: return { 'url_src': google_values['thumbnailLink'], 'title': google_values['title'], } values = { 'name': google_values['title'], 'image_1920': self._fetch_data( google_values['thumbnailLink'].replace('=s220', ''), {}, 'image')['values'], 'mime_type': google_values['mimeType'], 'document_id': document_id, } if google_values['mimeType'].startswith('video/'): values['slide_type'] = 'video' elif google_values['mimeType'].startswith('image/'): values['datas'] = values['image_1920'] values['slide_type'] = 'infographic' elif google_values['mimeType'].startswith( 'application/vnd.google-apps'): values['slide_type'] = get_slide_type(values) if 'exportLinks' in google_values: values['datas'] = self._fetch_data( google_values['exportLinks']['application/pdf'], params, 'pdf')['values'] elif google_values['mimeType'] == 'application/pdf': # TODO: Google Drive PDF document doesn't provide plain text transcript values['datas'] = self._fetch_data(google_values['webContentLink'], {}, 'pdf')['values'] values['slide_type'] = get_slide_type(values) return {'values': values} def _default_website_meta(self): res = super(Slide, self)._default_website_meta() res['default_opengraph']['og:title'] = res['default_twitter'][ 'twitter:title'] = self.name res['default_opengraph']['og:description'] = res['default_twitter'][ 'twitter:description'] = self.description res['default_opengraph']['og:image'] = res['default_twitter'][ 'twitter:image'] = self.env['website'].image_url( self, 'image_1024') res['default_meta_description'] = self.description return res # --------------------------------------------------------- # Data / Misc # --------------------------------------------------------- def get_backend_menu_id(self): return self.env.ref('website_slides.website_slides_menu_root').id
class HrEmployee(models.Model): _inherit = 'hr.employee' gender = fields.Selection(selection_add=[('transgender', 'Transgender')])
class PurchaseRequisition(models.Model): _name = "purchase.requisition" _description = "Purchase Requisition" _inherit = ['mail.thread'] _order = "id desc" def _get_picking_in(self): pick_in = self.env.ref('stock.picking_type_in') if not pick_in: company = self.env['res.company']._company_default_get( 'purchase.requisition') pick_in = self.env['stock.picking.type'].search( [('warehouse_id.company_id', '=', company.id), ('code', '=', 'incoming')], limit=1, ) return pick_in def _get_type_id(self): return self.env['purchase.requisition.type'].search([], limit=1) name = fields.Char(string='Agreement Reference', required=True, copy=False, default='New', readonly=True) origin = fields.Char(string='Source Document') order_count = fields.Integer(compute='_compute_orders_number', string='Number of Orders') vendor_id = fields.Many2one('res.partner', string="Vendor") type_id = fields.Many2one('purchase.requisition.type', string="Agreement Type", required=True, default=_get_type_id) ordering_date = fields.Date(string="Ordering Date", track_visibility='onchange') date_end = fields.Datetime(string='Agreement Deadline', track_visibility='onchange') schedule_date = fields.Date( string='Delivery Date', index=True, help= "The expected and scheduled delivery date where all the products are received", track_visibility='onchange') user_id = fields.Many2one('res.users', string='Purchase Representative', default=lambda self: self.env.user) description = fields.Text() company_id = fields.Many2one('res.company', string='Company', required=True, default=lambda self: self.env['res.company']. _company_default_get('purchase.requisition')) purchase_ids = fields.One2many('purchase.order', 'requisition_id', string='Purchase Orders', states={'done': [('readonly', True)]}) line_ids = fields.One2many('purchase.requisition.line', 'requisition_id', string='Products to Purchase', states={'done': [('readonly', True)]}, copy=True) warehouse_id = fields.Many2one('stock.warehouse', string='Warehouse') state = fields.Selection(PURCHASE_REQUISITION_STATES, 'Status', track_visibility='onchange', required=True, copy=False, default='draft') state_blanket_order = fields.Selection(PURCHASE_REQUISITION_STATES, compute='_set_state') account_analytic_id = fields.Many2one('account.analytic.account', 'Analytic Account') picking_type_id = fields.Many2one('stock.picking.type', 'Operation Type', required=True, default=_get_picking_in) is_quantity_copy = fields.Selection(related='type_id.quantity_copy', readonly=True) currency_id = fields.Many2one( 'res.currency', 'Currency', required=True, default=lambda self: self.env.user.company_id.currency_id.id) @api.depends('state') def _set_state(self): self.state_blanket_order = self.state @api.onchange('vendor_id') def _onchange_vendor(self): requisitions = self.env['purchase.requisition'].search([ ('vendor_id', '=', self.vendor_id.id), ('state', '=', 'ongoing'), ('type_id.quantity_copy', '=', 'none'), ]) if any(requisitions): title = _("Warning for %s") % self.vendor_id.name message = _( "There is already an open blanket order for this supplier. We suggest you to use to complete this open blanket order instead of creating a new one." ) warning = {'title': title, 'message': message} return {'warning': warning} @api.multi @api.depends('purchase_ids') def _compute_orders_number(self): for requisition in self: requisition.order_count = len(requisition.purchase_ids) @api.multi def action_cancel(self): # try to set all associated quotations to cancel state for requisition in self: for requisition_line in requisition.line_ids: requisition_line.supplier_info_ids.unlink() requisition.purchase_ids.button_cancel() for po in requisition.purchase_ids: po.message_post(body=_( 'Cancelled by the agreement associated to this quotation.') ) self.write({'state': 'cancel'}) @api.multi def action_in_progress(self): self.ensure_one() if not all(obj.line_ids for obj in self): raise UserError( _('You cannot confirm agreement because there is no product line.' )) if self.type_id.quantity_copy == 'none' and self.vendor_id: for requisition_line in self.line_ids: if requisition_line.price_unit <= 0.0: raise UserError( _('You cannot confirm the blanket order without price.' )) if requisition_line.product_qty <= 0.0: raise UserError( _('You cannot confirm the blanket order without quantity.' )) requisition_line.create_supplier_info() self.write({'state': 'ongoing'}) else: self.write({'state': 'in_progress'}) # Set the sequence number regarding the requisition type if self.name == 'New': if self.is_quantity_copy != 'none': self.name = self.env['ir.sequence'].next_by_code( 'purchase.requisition.purchase.tender') else: self.name = self.env['ir.sequence'].next_by_code( 'purchase.requisition.blanket.order') @api.multi def action_open(self): self.write({'state': 'open'}) def action_draft(self): self.ensure_one() self.name = 'New' self.write({'state': 'draft'}) @api.multi def action_done(self): """ Generate all purchase order based on selected lines, should only be called on one agreement at a time """ if any(purchase_order.state in ['draft', 'sent', 'to approve'] for purchase_order in self.mapped('purchase_ids')): raise UserError( _('You have to cancel or validate every RfQ before closing the purchase requisition.' )) for requisition in self: for requisition_line in requisition.line_ids: requisition_line.supplier_info_ids.unlink() self.write({'state': 'done'}) def _prepare_tender_values(self, product_id, product_qty, product_uom, location_id, name, origin, values): return { 'origin': origin, 'date_end': values['date_planned'], 'warehouse_id': values.get('warehouse_id') and values['warehouse_id'].id or False, 'company_id': values['company_id'].id, 'line_ids': [(0, 0, { 'product_id': product_id.id, 'product_uom_id': product_uom.id, 'product_qty': product_qty, 'move_dest_id': values.get('move_dest_ids') and values['move_dest_ids'][0].id or False, })], } def unlink(self): if any(requisition.state not in ('draft', 'cancel') for requisition in self): raise UserError(_('You can only delete draft requisitions.')) # Draft requisitions could have some requisition lines. self.mapped('line_ids').unlink() return super(PurchaseRequisition, self).unlink()
class Session(models.Model): _name = 'openacademy.session' _description = "OpenAcademy Sessions" name = fields.Char(required=True) start_date = fields.Date(default=fields.Date.today) duration = fields.Float(digits=(6, 2), help="Duration in days") seats = fields.Integer(string="Number of seats") active = fields.Boolean(default=True) color = fields.Integer() instructor_id = fields.Many2one('res.partner', string="Instructor", domain=[('instructor', '=', True)]) course_id = fields.Many2one('openacademy.course', ondelete='cascade', string="Course", required=True) attendee_ids = fields.Many2many('res.partner', string="Attendees") taken_seats = fields.Float(string="Taken seats", compute='_taken_seats') end_date = fields.Date(string="End Date", store=True, compute='_get_end_date', inverse='_set_end_date') attendees_count = fields.Integer( string="Attendees count", compute='_get_attendees_count', store=True) status = fields.Selection([ ('draft', "Draft"), ('started', "Started"), ('done', "Done"), ('cancelled', "Cancelled"), ], string="Progress", default='draft', translate=True) @api.depends('seats', 'attendee_ids') def _taken_seats(self): for r in self: if not r.seats: r.taken_seats = 0.0 else: r.taken_seats = (len(r.attendee_ids) / r.seats) * 100.0 @api.onchange('seats', 'attendee_ids') def _verify_valid_seats(self): if self.seats < 0: return { 'warning': { 'title': _("Incorrect 'seats' value"), 'message': _("The number of available seats may not be negative"), }, } if self.seats < len(self.attendee_ids): return { 'warning': { 'title': _("Too many attendees"), 'message': _("Increase seats or remove excess attendees"), }, } @api.depends('start_date', 'duration') def _get_end_date(self): for r in self: if not (r.start_date and r.duration): r.end_date = r.start_date continue # Add duration to start_date, but: Monday + 5 days = Saturday, so # subtract one second to get on Friday instead duration = timedelta(days=r.duration, seconds=-1) r.end_date = r.start_date + duration def _set_end_date(self): for r in self: if not (r.start_date and r.end_date): continue # Compute the difference between dates, but: Friday - Monday = 4 days, # so add one day to get 5 days instead r.duration = (r.end_date - r.start_date).days + 1 @api.depends('attendee_ids') def _get_attendees_count(self): for r in self: r.attendees_count = len(r.attendee_ids) @api.constrains('instructor_id', 'attendee_ids') def _check_instructor_not_in_attendees(self): for r in self: if r.instructor_id and r.instructor_id in r.attendee_ids: raise exceptions.ValidationError("A session's instructor can't be an attendee") def send_session_report(self): # Find the e-mail template template = self.env.ref('openacademy.openacademy_session_mail_template') # You can also find the e-mail template like this: # template = self.env['ir.model.data'].get_object('send_mail_template_demo', 'example_email_template') # Send out the e-mail template to the user self.env['mail.template'].browse(template.id).send_mail(self.id) def start_session(self): for record in self: record.status = 'started'
class Patient(models.Model): _name = "arc.patient" name = fields.Char(string="Name", required=True) patient_uid = fields.Char(string="Patient ID", readonly=True) image = fields.Binary(string="Image") small_image = fields.Binary(string="Small Image") person_id = fields.Many2one(comodel_name="arc.person", string="Person") identity_ids = fields.One2many(comodel_name="arc.identity", inverse_name="patient_id") # Contact Details email = fields.Char(string="e-Mail") mobile = fields.Char(string="Mobile") phone = fields.Char(string="Phone") # Alternate Contact alternate_contact_name = fields.Char(string="Name") alternate_contact_relation = fields.Char(string="Relation") alternate_mobile = fields.Char(string="Mobile") alternate_address = fields.Text(string="Address") # Address in Detail door_no = fields.Char(string="Door No") building_name = fields.Char(string="Building Name") street_1 = fields.Char(string="Street 1") street_2 = fields.Char(string="Street 2") locality = fields.Char(string="locality") landmark = fields.Char(string="landmark") city = fields.Char(string="City") state_id = fields.Many2one(comodel_name="res.country.state", string="State", default=lambda self: self.env.user.company_id.state_id.id) country_id = fields.Many2one(comodel_name="res.country", string="Country") pin_code = fields.Char(string="Pincode") # Personnel Details age = fields.Integer(string="Age") blood_group = fields.Selection(selection=BLOOD_GROUP, string="Blood Group") marital_status = fields.Selection(selection=MARITAL_STATUS, string="Marital Status") gender = fields.Selection(selection=GENDER, string="Gender") caste = fields.Char(string="Caste") religion_id = fields.Many2one(comodel_name="arc.religion", string="Religion") physically_challenged = fields.Boolean(string="Physically Challenged") nationality_id = fields.Many2one(comodel_name="res.country") mother_tongue_id = fields.Many2one(comodel_name="arc.language", string="Mother Tongue") language_known_ids = fields.Many2many(comodel_name="arc.language", string="Language Known") family_member_ids = fields.One2many(comodel_name="arc.address", inverse_name="patient_id") # Medical Detail allergic_towards = fields.Text(string="Allergic Towards") # Attachment attachment_ids = fields.Many2many(comodel_name="ir.attachment", string="Attachment") def update_person_address(self): recs = {} recs["name"] = self.name recs["person_uid"] = self.patient_uid recs["image"] = self.image recs["small_image"] = self.small_image recs["email"] = self.email recs["mobile"] = self.mobile recs["phone"] = self.phone recs["door_no"] = self.door_no recs["building_name"] = self.building_name recs["street_1"] = self.street_1 recs["street_2"] = self.street_2 recs["locality"] = self.locality recs["landmark"] = self.landmark recs["city"] = self.city recs["state_id"] = self.state_id.id recs["country_id"] = self.country_id.id recs["pin_code"] = self.pin_code recs["is_patient"] = True self.person_id.write(recs) @api.multi def write(self, vals): rec = super(Patient, self).write(vals) self.update_person_address() return rec @api.model def create(self, vals): data = {"person_uid": self.env["ir.sequence"].next_by_code(self._name), "is_patient": True, "name": vals["name"]} data.update(vals) person_id = self.env["arc.person"].create(data) vals["person_id"] = person_id.id vals["patient_uid"] = data["person_uid"] return super(Patient, self).create(vals)
class GitRepository(models.Model): _name = 'project.git.repository' def _default_secret(self): alphabet = string.ascii_letters + string.digits secret = ''.join( random.SystemRandom().choice(alphabet) for i in range(30) ) return secret name = fields.Char( string='Name', size=256, ) repo_name = fields.Char(related="name") uuid = fields.Char( string='UUID', size=256, index=True, ) full_name = fields.Char( string='Full Name', size=256 ) odoo_uuid = fields.Char( string='UUID', size=256, default=lambda *a: uuid.uuid4(), index=True, ) avatar = fields.Char( string='Avatar', compute='_compute_avatar', ) url = fields.Char( string='URL', default='#' ) project_id = fields.Many2one( comodel_name='project.project', string='Project', required=True, index=True, ) branch_ids = fields.One2many( comodel_name='project.git.branch', string='Branches', inverse_name='repository_id' ) branch_count = fields.Integer( compute="_compute_branch_count" ) user_id = fields.Many2one( comodel_name='project.git.user', string='Owner', ondelete='cascade', index=True, ) type = fields.Selection( selection=[], string='Type', index=True, ) webhook_url = fields.Char( string='Webhook Url', compute='_compute_webhook_url', ) secret = fields.Char( default=lambda s: s._default_secret() ) use_secret = fields.Boolean( compute='_compute_use_secret' ) image_type = fields.Char( string='Type', compute='_compute_image_type' ) @api.multi @api.depends("branch_ids") def _compute_branch_count(self): for rec in self: rec.branch_count = len(rec.branch_ids) @api.multi @api.depends('type') def _compute_avatar(self): get_avatar(self, 'repository') @api.multi @api.depends('type') def _compute_use_secret(self): secret_types = self._secret_visible_for_types() for rec in self: rec.use_secret = rec.type in secret_types def _secret_visible_for_types(self): return [] @api.multi @api.depends('type') def _compute_image_type(self): get_image_type(self) @api.onchange('project_id', 'type') def _onchange_name_components(self): if not self.project_id or self.type: return self.name = '%s - %s' % ( self.project_id.key, self._get_selection_label(self.type) ) def _get_selection_label(self, type): for item in self._fields['type'].selection: if item[0] == type: return item[1] return '' @api.multi @api.depends('odoo_uuid', 'type') def _compute_webhook_url(self): base_url = self.env['ir.config_parameter']\ .sudo()\ .get_param('web.base.url') for record in self: if record.type: record.webhook_url = urljoin( base_url, record.type, 'payload', record.odoo_uuid )
class PurchaseOrderLine(models.Model): _inherit = 'purchase.order.line' qty_received_method = fields.Selection(selection_add=[('stock_moves', 'Stock Moves')]) move_ids = fields.One2many('stock.move', 'purchase_line_id', string='Reservation', readonly=True, copy=False) orderpoint_id = fields.Many2one('stock.warehouse.orderpoint', 'Orderpoint', copy=False, index=True) move_dest_ids = fields.One2many('stock.move', 'created_purchase_line_id', 'Downstream Moves') product_description_variants = fields.Char('Custom Description') propagate_cancel = fields.Boolean('Propagate cancellation', default=True) forecasted_issue = fields.Boolean(compute='_compute_forecasted_issue') def _compute_qty_received_method(self): super(PurchaseOrderLine, self)._compute_qty_received_method() for line in self.filtered(lambda l: not l.display_type): if line.product_id.type in ['consu', 'product']: line.qty_received_method = 'stock_moves' def _get_po_line_moves(self): self.ensure_one() moves = self.move_ids.filtered(lambda m: m.product_id == self.product_id) if self._context.get('accrual_entry_date'): moves = moves.filtered(lambda r: fields.Date.to_date(r.date) <= self._context['accrual_entry_date']) return moves @api.depends('move_ids.state', 'move_ids.product_uom_qty', 'move_ids.product_uom') def _compute_qty_received(self): super(PurchaseOrderLine, self)._compute_qty_received() for line in self: if line.qty_received_method == 'stock_moves': total = 0.0 # In case of a BOM in kit, the products delivered do not correspond to the products in # the PO. Therefore, we can skip them since they will be handled later on. for move in line._get_po_line_moves(): if move.state == 'done': if move.location_dest_id.usage == "supplier": if move.to_refund: total -= move.product_uom._compute_quantity(move.product_uom_qty, line.product_uom, rounding_method='HALF-UP') elif move.origin_returned_move_id and move.origin_returned_move_id._is_dropshipped() and not move._is_dropshipped_returned(): # Edge case: the dropship is returned to the stock, no to the supplier. # In this case, the received quantity on the PO is set although we didn't # receive the product physically in our stock. To avoid counting the # quantity twice, we do nothing. pass elif ( move.location_dest_id.usage == "internal" and move.to_refund and move.location_dest_id not in self.env["stock.location"].search( [("id", "child_of", move.warehouse_id.view_location_id.id)] ) ): total -= move.product_uom._compute_quantity(move.product_uom_qty, line.product_uom, rounding_method='HALF-UP') else: total += move.product_uom._compute_quantity(move.product_uom_qty, line.product_uom, rounding_method='HALF-UP') line._track_qty_received(total) line.qty_received = total @api.depends('product_uom_qty', 'date_planned') def _compute_forecasted_issue(self): for line in self: warehouse = line.order_id.picking_type_id.warehouse_id line.forecasted_issue = False if line.product_id: virtual_available = line.product_id.with_context(warehouse=warehouse.id, to_date=line.date_planned).virtual_available if line.state == 'draft': virtual_available += line.product_uom_qty if virtual_available < 0: line.forecasted_issue = True @api.model_create_multi def create(self, vals_list): lines = super(PurchaseOrderLine, self).create(vals_list) lines.filtered(lambda l: l.order_id.state == 'purchase')._create_or_update_picking() return lines def write(self, values): if values.get('date_planned'): new_date = fields.Datetime.to_datetime(values['date_planned']) self.filtered(lambda l: not l.display_type)._update_move_date_deadline(new_date) lines = self.filtered(lambda l: l.order_id.state == 'purchase') previous_product_qty = {line.id: line.product_uom_qty for line in lines} result = super(PurchaseOrderLine, self).write(values) if 'product_qty' in values: lines.with_context(previous_product_qty=previous_product_qty)._create_or_update_picking() return result def action_product_forecast_report(self): self.ensure_one() action = self.product_id.action_product_forecast_report() action['context'] = { 'active_id': self.product_id.id, 'active_model': 'product.product', 'move_to_match_ids': self.move_ids.filtered(lambda m: m.product_id == self.product_id).ids, 'purchase_line_to_match_id': self.id, } warehouse = self.order_id.picking_type_id.warehouse_id if warehouse: action['context']['warehouse'] = warehouse.id return action def unlink(self): self.move_ids._action_cancel() ppg_cancel_lines = self.filtered(lambda line: line.propagate_cancel) ppg_cancel_lines.move_dest_ids._action_cancel() not_ppg_cancel_lines = self.filtered(lambda line: not line.propagate_cancel) not_ppg_cancel_lines.move_dest_ids.write({'procure_method': 'make_to_stock'}) not_ppg_cancel_lines.move_dest_ids._recompute_state() return super().unlink() # -------------------------------------------------- # Business methods # -------------------------------------------------- def _update_move_date_deadline(self, new_date): """ Updates corresponding move picking line deadline dates that are not yet completed. """ moves_to_update = self.move_ids.filtered(lambda m: m.state not in ('done', 'cancel')) if not moves_to_update: moves_to_update = self.move_dest_ids.filtered(lambda m: m.state not in ('done', 'cancel')) for move in moves_to_update: move.date_deadline = new_date + relativedelta(days=move.company_id.po_lead) def _create_or_update_picking(self): for line in self: if line.product_id and line.product_id.type in ('product', 'consu'): # Prevent decreasing below received quantity if float_compare(line.product_qty, line.qty_received, line.product_uom.rounding) < 0: raise UserError(_('You cannot decrease the ordered quantity below the received quantity.\n' 'Create a return first.')) if float_compare(line.product_qty, line.qty_invoiced, line.product_uom.rounding) == -1: # If the quantity is now below the invoiced quantity, create an activity on the vendor bill # inviting the user to create a refund. line.invoice_lines[0].move_id.activity_schedule( 'mail.mail_activity_data_warning', note=_('The quantities on your purchase order indicate less than billed. You should ask for a refund.')) # If the user increased quantity of existing line or created a new line pickings = line.order_id.picking_ids.filtered(lambda x: x.state not in ('done', 'cancel') and x.location_dest_id.usage in ('internal', 'transit', 'customer')) picking = pickings and pickings[0] or False if not picking: res = line.order_id._prepare_picking() picking = self.env['stock.picking'].create(res) moves = line._create_stock_moves(picking) moves._action_confirm()._action_assign() def _get_stock_move_price_unit(self): self.ensure_one() order = self.order_id price_unit = self.price_unit price_unit_prec = self.env['decimal.precision'].precision_get('Product Price') if self.taxes_id: qty = self.product_qty or 1 price_unit = self.taxes_id.with_context(round=False).compute_all( price_unit, currency=self.order_id.currency_id, quantity=qty, product=self.product_id, partner=self.order_id.partner_id )['total_void'] price_unit = float_round(price_unit / qty, precision_digits=price_unit_prec) if self.product_uom.id != self.product_id.uom_id.id: price_unit *= self.product_uom.factor / self.product_id.uom_id.factor if order.currency_id != order.company_id.currency_id: price_unit = order.currency_id._convert( price_unit, order.company_id.currency_id, self.company_id, self.date_order or fields.Date.today(), round=False) return price_unit def _prepare_stock_moves(self, picking): """ Prepare the stock moves data for one order line. This function returns a list of dictionary ready to be used in stock.move's create() """ self.ensure_one() res = [] if self.product_id.type not in ['product', 'consu']: return res price_unit = self._get_stock_move_price_unit() qty = self._get_qty_procurement() move_dests = self.move_dest_ids if not move_dests: move_dests = self.move_ids.move_dest_ids.filtered(lambda m: m.state != 'cancel' and not m.location_dest_id.usage == 'supplier') if not move_dests: qty_to_attach = 0 qty_to_push = self.product_qty - qty else: move_dests_initial_demand = self.product_id.uom_id._compute_quantity( sum(move_dests.filtered(lambda m: m.state != 'cancel' and not m.location_dest_id.usage == 'supplier').mapped('product_qty')), self.product_uom, rounding_method='HALF-UP') qty_to_attach = move_dests_initial_demand - qty qty_to_push = self.product_qty - move_dests_initial_demand if float_compare(qty_to_attach, 0.0, precision_rounding=self.product_uom.rounding) > 0: product_uom_qty, product_uom = self.product_uom._adjust_uom_quantities(qty_to_attach, self.product_id.uom_id) res.append(self._prepare_stock_move_vals(picking, price_unit, product_uom_qty, product_uom)) if not float_is_zero(qty_to_push, precision_rounding=self.product_uom.rounding): product_uom_qty, product_uom = self.product_uom._adjust_uom_quantities(qty_to_push, self.product_id.uom_id) extra_move_vals = self._prepare_stock_move_vals(picking, price_unit, product_uom_qty, product_uom) extra_move_vals['move_dest_ids'] = False # don't attach res.append(extra_move_vals) return res def _get_qty_procurement(self): self.ensure_one() qty = 0.0 outgoing_moves, incoming_moves = self._get_outgoing_incoming_moves() for move in outgoing_moves: qty -= move.product_uom._compute_quantity(move.product_uom_qty, self.product_uom, rounding_method='HALF-UP') for move in incoming_moves: qty += move.product_uom._compute_quantity(move.product_uom_qty, self.product_uom, rounding_method='HALF-UP') return qty def _check_orderpoint_picking_type(self): warehouse_loc = self.order_id.picking_type_id.warehouse_id.view_location_id dest_loc = self.move_dest_ids.location_id or self.orderpoint_id.location_id if warehouse_loc and dest_loc and not warehouse_loc.parent_path in dest_loc[0].parent_path: raise UserError(_('For the product %s, the warehouse of the operation type (%s) is inconsistent with the location (%s) of the reordering rule (%s). Change the operation type or cancel the request for quotation.', self.product_id.display_name, self.order_id.picking_type_id.display_name, self.orderpoint_id.location_id.display_name, self.orderpoint_id.display_name)) def _prepare_stock_move_vals(self, picking, price_unit, product_uom_qty, product_uom): self.ensure_one() self._check_orderpoint_picking_type() product = self.product_id.with_context(lang=self.order_id.dest_address_id.lang or self.env.user.lang) date_planned = self.date_planned or self.order_id.date_planned return { # truncate to 2000 to avoid triggering index limit error # TODO: remove index in master? 'name': (self.name or '')[:2000], 'product_id': self.product_id.id, 'date': date_planned, 'date_deadline': date_planned + relativedelta(days=self.order_id.company_id.po_lead), 'location_id': self.order_id.partner_id.property_stock_supplier.id, 'location_dest_id': (self.orderpoint_id and not (self.move_ids | self.move_dest_ids)) and self.orderpoint_id.location_id.id or self.order_id._get_destination_location(), 'picking_id': picking.id, 'partner_id': self.order_id.dest_address_id.id, 'move_dest_ids': [(4, x) for x in self.move_dest_ids.ids], 'state': 'draft', 'purchase_line_id': self.id, 'company_id': self.order_id.company_id.id, 'price_unit': price_unit, 'picking_type_id': self.order_id.picking_type_id.id, 'group_id': self.order_id.group_id.id, 'origin': self.order_id.name, 'description_picking': product.description_pickingin or self.name, 'propagate_cancel': self.propagate_cancel, 'warehouse_id': self.order_id.picking_type_id.warehouse_id.id, 'product_uom_qty': product_uom_qty, 'product_uom': product_uom.id, 'product_packaging_id': self.product_packaging_id.id, } @api.model def _prepare_purchase_order_line_from_procurement(self, product_id, product_qty, product_uom, company_id, values, po): line_description = '' if values.get('product_description_variants'): line_description = values['product_description_variants'] supplier = values.get('supplier') res = self._prepare_purchase_order_line(product_id, product_qty, product_uom, company_id, supplier, po) # We need to keep the vendor name set in _prepare_purchase_order_line. To avoid redundancy # in the line name, we add the line_description only if different from the product name. # This way, we shoud not lose any valuable information. if line_description and product_id.name != line_description: res['name'] += '\n' + line_description res['move_dest_ids'] = [(4, x.id) for x in values.get('move_dest_ids', [])] res['orderpoint_id'] = values.get('orderpoint_id', False) and values.get('orderpoint_id').id res['propagate_cancel'] = values.get('propagate_cancel') res['product_description_variants'] = values.get('product_description_variants') return res def _create_stock_moves(self, picking): values = [] for line in self.filtered(lambda l: not l.display_type): for val in line._prepare_stock_moves(picking): values.append(val) line.move_dest_ids.created_purchase_line_id = False return self.env['stock.move'].create(values) def _find_candidate(self, product_id, product_qty, product_uom, location_id, name, origin, company_id, values): """ Return the record in self where the procument with values passed as args can be merged. If it returns an empty record then a new line will be created. """ description_picking = '' if values.get('product_description_variants'): description_picking = values['product_description_variants'] lines = self.filtered( lambda l: l.propagate_cancel == values['propagate_cancel'] and ((values['orderpoint_id'] and not values['move_dest_ids']) and l.orderpoint_id == values['orderpoint_id'] or True) ) # In case 'product_description_variants' is in the values, we also filter on the PO line # name. This way, we can merge lines with the same description. To do so, we need the # product name in the context of the PO partner. if lines and values.get('product_description_variants'): partner = self.mapped('order_id.partner_id')[:1] product_lang = product_id.with_context( lang=partner.lang, partner_id=partner.id, ) name = product_lang.display_name if product_lang.description_purchase: name += '\n' + product_lang.description_purchase lines = lines.filtered(lambda l: l.name == name + '\n' + description_picking) if lines: return lines[0] return lines and lines[0] or self.env['purchase.order.line'] def _get_outgoing_incoming_moves(self): outgoing_moves = self.env['stock.move'] incoming_moves = self.env['stock.move'] for move in self.move_ids.filtered(lambda r: r.state != 'cancel' and not r.scrapped and self.product_id == r.product_id): if move.location_dest_id.usage == "supplier" and move.to_refund: outgoing_moves |= move elif move.location_dest_id.usage != "supplier": if not move.origin_returned_move_id or (move.origin_returned_move_id and move.to_refund): incoming_moves |= move return outgoing_moves, incoming_moves def _update_date_planned(self, updated_date): move_to_update = self.move_ids.filtered(lambda m: m.state not in ['done', 'cancel']) if not self.move_ids or move_to_update: # Only change the date if there is no move done or none super()._update_date_planned(updated_date) if move_to_update: self._update_move_date_deadline(updated_date) @api.model def _update_qty_received_method(self): """Update qty_received_method for old PO before install this module.""" self.search([])._compute_qty_received_method()
class HelpdeskStageCustom(models.Model): _name = "helpdesk.stage.custom" _order = 'priority desc' priority = fields.Integer(string='Priority', default=1, required=True) send_mail = fields.Boolean('Send mail') template_id = fields.Many2one('mail.template', string='Template') helpdesk_stage_id = fields.Many2one('helpdesk.stage.config', string='Stage') action_when = fields.Selection([('before', 'Before'), ('after', 'After')], string='Before/After', required=True, default='before') mail_action = fields.Selection([('remainder', 'FollowUp Mail'), ('warning', 'Reminder Mail')], string='Mail Action') sla_level_id = fields.Many2one('sla.level.configuration', string='SLA Levels') action_time = fields.Selection([ ('1_m', '1 Minute'), ('5_m', '5 Minutes'), ('15_m', '15 Minutes'), ('30_m', '30 Minutes'), ('1_h', '1 Hour'), ('2_h', '2 Hours'), ('4_h', '4 Hours'), ('8_h', '8 Hours'), ('1_d', '1 Day'), ('2_d', '2 Days'), ('3_d', '3 Days'), ('4_d', '4 Days'), ('5_d', '5 Days'), ('7_d', '7 Days'), ('10_d', '10 Days'), ('20_d', '20 Days'), ('30_d', '30 Days'), ('180_d', '180 Days'), ('365_d', '365 Days'), ], string='Time', required=True) action_color = fields.Selection([('orange', 'Orange'), ('red', 'Red'), ('green', 'Green'), ('purple', 'Purple'), ('yellow', 'Yellow'), ('blue', 'Blue')], string='Colors', required=True) action_perform = fields.Selection( [('request_date', 'Creation Date'), ('last_stage_update', 'Last Stage Update'), ('assign_date', 'Assign Date'), ('dead_line_date', 'Deadline Date'), ('close_date', 'Closed'), ('message_last_post', 'Last Message Date')], string='Field', required=True) @api.one @api.constrains('action_time', 'action_perform') def _check_action_time(self): if self.action_perform in _MAP_FIELDS_DATE and self.action_time in [ '1_m', '5_m', '15_m', '30_m', '1_h', '2_h', '4_h', '8_h' ]: raise Warning(_('Misconfiguration, check Field and Time.'))
class PaymentAcquirer(models.Model): _inherit = 'payment.acquirer' provider = fields.Selection(selection_add=[('payulatam', 'PayU Latam')], ondelete={'payulatam': 'set default'}) payulatam_merchant_id = fields.Char( string="PayU Latam Merchant ID", help="The ID solely used to identify the account with PayULatam", required_if_provider='payulatam') payulatam_account_id = fields.Char( string="PayU Latam Account ID", help= "The ID solely used to identify the country-dependent shop with PayULatam", required_if_provider='payulatam') payulatam_api_key = fields.Char(string="PayU Latam API Key", required_if_provider='payulatam', groups='base.group_system') @api.model def _get_compatible_acquirers(self, *args, currency_id=None, **kwargs): """ Override of payment to unlist PayU Latam acquirers for unsupported currencies. """ acquirers = super()._get_compatible_acquirers(*args, currency_id=currency_id, **kwargs) currency = self.env['res.currency'].browse(currency_id).exists() if currency and currency.name not in SUPPORTED_CURRENCIES: acquirers = acquirers.filtered(lambda a: a.provider != 'payulatam') return acquirers def _payulatam_generate_sign(self, values, incoming=True): """ Generate the signature for incoming or outgoing communications. :param dict values: The values used to generate the signature :param bool incoming: Whether the signature must be generated for an incoming (PayU Latam to Odoo) or outgoing (Odoo to PayU Latam) communication. :return: The signature :rtype: str """ if incoming: data_string = '~'.join([ self.payulatam_api_key, self.payulatam_merchant_id, values['referenceCode'], # Observation from the sandbox: PayU Latam is using a rounded value to generate # their signature despite saying otherwise in the doc: # http://developers.payulatam.com/en/web_checkout/integration.html float_repr(float(values.get('TX_VALUE')), 1), values['currency'], values.get('transactionState'), ]) else: data_string = '~'.join([ self.payulatam_api_key, self.payulatam_merchant_id, values['referenceCode'], float_repr(float(values['amount']), 1), values['currency'], ]) return md5(data_string.encode('utf-8')).hexdigest()
class PurchaseReport(models.Model): _inherit = "purchase.report" fiscal_operation_id = fields.Many2one( comodel_name="l10n_br_fiscal.operation", string="Fiscal Operation", readonly=True, ) fiscal_operation_line_id = fields.Many2one( comodel_name="l10n_br_fiscal.operation.line", string="Fiscal Operation Line", readonly=True, ) cfop_id = fields.Many2one( comodel_name="l10n_br_fiscal.cfop", string="CFOP", ) fiscal_type = fields.Selection(selection=PRODUCT_FISCAL_TYPE, string="Tipo Fiscal") cest_id = fields.Many2one( comodel_name="l10n_br_fiscal.cest", string="CEST", ) ncm_id = fields.Many2one(comodel_name="l10n_br_fiscal.ncm", string="NCM") nbm_id = fields.Many2one(comodel_name="l10n_br_fiscal.nbm", string="NBM") icms_value = fields.Float( string="ICMS Value", digits=dp.get_precision("Account"), ) icmsst_value = fields.Float( string="ICMS ST Value", digits=dp.get_precision("Account"), ) ipi_value = fields.Float( string="IPI Value", digits=dp.get_precision("Account"), ) pis_value = fields.Float( string="PIS Value", digits=dp.get_precision("Account"), ) cofins_value = fields.Float( string="COFINS Value", digits=dp.get_precision("Account"), ) ii_value = fields.Float( string="II Value", digits=dp.get_precision("Account"), ) freight_value = fields.Float( string="Freight Value", digits=dp.get_precision("Account"), ) insurance_value = fields.Float( string="Insurance Value", digits=dp.get_precision("Account"), ) other_value = fields.Float( string="Other Value", digits=dp.get_precision("Account"), ) total_with_taxes = fields.Float( string="Total with Taxes", digits=dp.get_precision("Account"), ) def _select(self): select_str = super()._select() select_str += """ , l.fiscal_operation_id as fiscal_operation_id , l.fiscal_operation_line_id as fiscal_operation_line_id , l.cfop_id , l.fiscal_type , l.ncm_id , l.nbm_id , l.cest_id , SUM(l.icms_value) as icms_value , SUM(l.icmsst_value) as icmsst_value , SUM(l.ipi_value) as ipi_value , SUM(l.pis_value) as pis_value , SUM(l.cofins_value) as cofins_value , SUM(l.ii_value) as ii_value , SUM(l.freight_value) as freight_value , SUM(l.insurance_value) as insurance_value , SUM(l.other_value) as other_value , SUM(l.price_unit / COALESCE(NULLIF(cr.rate, 0), 1.0) * l.product_qty )::decimal(16,2) + SUM(CASE WHEN l.ipi_value IS NULL THEN 0.00 ELSE l.ipi_value END) + SUM(CASE WHEN l.icmsst_value IS NULL THEN 0.00 ELSE l.icmsst_value END) + SUM(CASE WHEN l.freight_value IS NULL THEN 0.00 ELSE l.freight_value END) + SUM(CASE WHEN l.insurance_value IS NULL THEN 0.00 ELSE l.insurance_value END) + SUM(CASE WHEN l.other_value IS NULL THEN 0.00 ELSE l.other_value END) as total_with_taxes """ return select_str def _group_by(self): group_by_str = super()._group_by() group_by_str += """ , l.fiscal_operation_id , l.fiscal_operation_line_id , l.cfop_id , l.fiscal_type , l.ncm_id , l.nbm_id , l.cest_id """ return group_by_str
class MrpRoutingWorkcenter(models.Model): _name = 'mrp.routing.workcenter' _description = 'Work Center Usage' _order = 'bom_id, sequence, id' _check_company_auto = True name = fields.Char('Operation', required=True) active = fields.Boolean(default=True) workcenter_id = fields.Many2one('mrp.workcenter', 'Work Center', required=True, check_company=True) sequence = fields.Integer( 'Sequence', default=100, help="Gives the sequence order when displaying a list of routing Work Centers.") bom_id = fields.Many2one( 'mrp.bom', 'Bill of Material', index=True, ondelete='cascade', required=True, help="The Bill of Material this operation is linked to") company_id = fields.Many2one('res.company', 'Company', related='bom_id.company_id') worksheet_type = fields.Selection([ ('pdf', 'PDF'), ('google_slide', 'Google Slide'), ('text', 'Text')], string="Work Sheet", default="text", help="Defines if you want to use a PDF or a Google Slide as work sheet." ) note = fields.Text('Description', help="Text worksheet description") worksheet = fields.Binary('PDF') worksheet_google_slide = fields.Char('Google Slide', help="Paste the url of your Google Slide. Make sure the access to the document is public.") time_mode = fields.Selection([ ('auto', 'Compute based on tracked time'), ('manual', 'Set duration manually')], string='Duration Computation', default='manual') time_mode_batch = fields.Integer('Based on', default=10) time_computed_on = fields.Char('Computed on last', compute='_compute_time_computed_on') time_cycle_manual = fields.Float( 'Manual Duration', default=60, help="Time in minutes:" "- In manual mode, time used" "- In automatic mode, supposed first time when there aren't any work orders yet") time_cycle = fields.Float('Duration', compute="_compute_time_cycle") workorder_count = fields.Integer("# Work Orders", compute="_compute_workorder_count") workorder_ids = fields.One2many('mrp.workorder', 'operation_id', string="Work Orders") @api.depends('time_mode', 'time_mode_batch') def _compute_time_computed_on(self): for operation in self: operation.time_computed_on = _('%i work orders') % operation.time_mode_batch if operation.time_mode != 'manual' else False @api.depends('time_cycle_manual', 'time_mode', 'workorder_ids') def _compute_time_cycle(self): manual_ops = self.filtered(lambda operation: operation.time_mode == 'manual') for operation in manual_ops: operation.time_cycle = operation.time_cycle_manual for operation in self - manual_ops: data = self.env['mrp.workorder'].read_group([ ('operation_id', '=', operation.id), ('qty_produced', '>', 0), ('state', '=', 'done')], ['operation_id', 'duration', 'qty_produced'], ['operation_id'], limit=operation.time_mode_batch) count_data = dict((item['operation_id'][0], (item['duration'], item['qty_produced'])) for item in data) if count_data.get(operation.id) and count_data[operation.id][1]: operation.time_cycle = (count_data[operation.id][0] / count_data[operation.id][1]) * (operation.workcenter_id.capacity or 1.0) else: operation.time_cycle = operation.time_cycle_manual def _compute_workorder_count(self): data = self.env['mrp.workorder'].read_group([ ('operation_id', 'in', self.ids), ('state', '=', 'done')], ['operation_id'], ['operation_id']) count_data = dict((item['operation_id'][0], item['operation_id_count']) for item in data) for operation in self: operation.workorder_count = count_data.get(operation.id, 0)
class AccountInvoice(models.Model): _inherit = 'account.invoice' charge_code_id = fields.Many2one('purchase.charge.code', string='Charge Code', compute='_compute_project_code', store=True) @api.depends('origin') def _compute_project_code(self): for record in self: source = record.env['purchase.order'].search([('name', '=', record.origin)], limit=1) record.charge_code_id = source.charge_code_id state = fields.Selection([ ('draft','Draft'), ('to_approve','Ready for Approval'), ('open', 'Approved'), ('in_payment', 'In Payment'), ('paid', 'Exported to QB'), ('cancel', 'Cancelled'), ], string='Status', index=True, readonly=True, default='draft', track_visibility='onchange', copy=False, help=" * The 'Draft' status is used when a user is encoding a new and unconfirmed Invoice.\n" " * The 'Ready for Approval' status is used when user creates invoice, an invoice number is generated but the Accounting Manager still needs to approve the invoice.\n" " * The 'Approved' status is used when the invoice needs to paid by the customer.\n" " * The 'In Payment' status is used when payments have been registered for the entirety of the invoice in a journal configured to post entries at bank reconciliation only, and some of them haven't been reconciled with a bank statement line yet.\n" " * The 'Exported to QB' status is set automatically when the invoice is paid. Its related journal entries may or may not be reconciled.\n" " * The 'Cancelled' status is used when user cancel invoice.") @api.model def _unlink_confirm_invoice_action(self): self.env.ref('account.action_account_invoice_confirm').unlink() @api.multi def generate_export_data(self, export_sequence): header = ['Export #', 'Bill #', 'PO #', 'Charge Code', 'Project Code', 'Vendor Name', 'Total'] # Get the bill lines that have never been exported before. See comment below new_export = self.env['account.invoice.line'].search([ ('invoice_id', 'in', self.ids), ('export_sequence', 'in', ('', False)) ]) new_export.write({'export_sequence': export_sequence}) # not sure if customer wants to export already exported lines or not # so for now, only export new lines content = [] for bill in self: # since they want to group by bill bill_group = {} for line in bill.invoice_line_ids.filtered(lambda l: l.export_sequence == export_sequence): key = (line.export_sequence, line.invoice_id.number, line.purchase_id.name, line.invoice_id.charge_code_id.name, line.project_code.name, line.invoice_id.partner_id.name) if not bill_group.get(key): bill_group[key] = 0.0 bill_group[key] += line.price_total # assuming they are using the same currency here, might need to revise if they want multicurrency content.extend([list(k) + [bill_group.get(k)] for k in bill_group.keys()]) data = [header] + content return _csv_write_rows(data) @api.multi def action_export(self): print('Exporting') self = self.env['account.invoice'].search([ ('id', 'in', self.ids), ('state', 'not in', ('draft', 'cancel')), ('type', '=', 'in_invoice'), ]) if not self: return {} export_sequence = self.env['ir.sequence'].next_by_code('export.vendor.bill') if not export_sequence: raise ValidationError(_('Please define sequence for export vendor bill')) Attachment = self.env['ir.attachment'].sudo() attachment_name = 'Vendor_Bill_Batch_{}.csv'.format(export_sequence) data = self.generate_export_data(export_sequence) attachment_vals = { 'name': attachment_name, 'datas': base64.encodestring(data), 'datas_fname': attachment_name, 'res_model': 'account.invoice', } Attachment.search([('name', '=', attachment_name)]).unlink() attachment = Attachment.create(attachment_vals) return { 'type': 'ir.actions.act_url', 'url': '/web/content/{}?download=true'.format(attachment.id), 'target': 'self' } @api.multi def action_invoice_to_approve(self): # Method similar to action_invoice_open but before approval stage to_approve_invoices = self.filtered(lambda inv: inv.state != 'open') if to_approve_invoices.filtered(lambda inv: not inv.partner_id): raise UserError(_("The field Vendor is required, please complete it to request approval of the Vendor Bill.")) if to_approve_invoices.filtered(lambda inv: inv.state != 'draft'): raise UserError(_("Invoice must be in draft state in order to request approval of the Accounting Manager.")) return self.write({'state': 'to_approve'}) @api.multi def action_invoice_open(self): # lots of duplicate calls to action_invoice_open, so we remove those already open to_open_invoices = self.filtered(lambda inv: inv.state != 'open') if to_open_invoices.filtered(lambda inv: not inv.partner_id): raise UserError(_("The field Vendor is required, please complete it to validate the Vendor Bill.")) if to_open_invoices.filtered(lambda inv: inv.state != 'to_approve'): raise UserError(_("Invoice must be in Ready to Approve state in order to validate it.")) if to_open_invoices.filtered(lambda inv: float_compare(inv.amount_total, 0.0, precision_rounding=inv.currency_id.rounding) == -1): raise UserError(_("You cannot validate an invoice with a negative total amount. You should create a credit note instead.")) if to_open_invoices.filtered(lambda inv: not inv.account_id): raise UserError(_('No account was found to create the invoice, be sure you have installed a chart of account.')) to_open_invoices.action_date_assign() to_open_invoices.action_move_create() return to_open_invoices.invoice_validate()
class marfrig_service_base(models.Model): _name = "deposito.service.base" _inherit = ['mail.thread', 'mail.activity.mixin', 'portal.mixin'] _description = "Deposito" _order = "id DESC" @api.depends('invoices_ids', 'invoice_count') def _compute_invoice(self): return name = fields.Char(default='Borrador') referencia = fields.Char(defualt='Referencia de Carpeta') stock_operation = fields.Many2one(comodel_name='stock.picking', string='Movimiento Deposito') user_id = fields.Many2one('res.users', string='Usuario', default=lambda self: self.env.user, track_visibility="onchange") company_id = fields.Many2one('res.company', string='Compañia', default=lambda self: self.env.user.company_id) partner_invoice_id = fields.Many2one(comodel_name='res.partner', string='Cliente', domain=[('customer', '=', True)]) pricelist_id = fields.Many2one('product.pricelist', string='Tarifa') currency_id = fields.Many2one(comodel_name="res.currency", string="Moneda", related="pricelist_id.currency_id", index=True, readonly=True, store=True) start_datetime = fields.Datetime(string='Fecha Inicio', required=True, index=True, copy=False, default=fields.datetime.now()) stop_datetime = fields.Datetime('Fecha Fin', index=True, copy=False) state = fields.Selection([ ('draft', 'Borrador'), ('confirm', 'Confirmado'), ('inprocess', 'En proceso'), ('invoiced', 'Facturado'), ('invoice_rejected', 'Fac. Rechazada'), ('cancel', 'Cancelado'), ('done', 'Realizado'), ], string='Status', index=True, readonly=True, default='draft', track_visibility='onchange', copy=False, store=True) dua_aduana = fields.Char(string='Mes', size=3) dua_anio = fields.Char(string='Año', size=4) dua_numero = fields.Char(string='Dua_Numero', size=6) deposito_srv_ids = fields.One2many('deposito.service.products', 'deposito_srv_id', string='Servicios', copy=True) aduana_destino_id = fields.Many2one('fronteras', 'Aduana Destino') origin_id = fields.Many2one(comodel_name='res.partner.address.ext', string='Origen') destiny_id = fields.Many2one(comodel_name='res.partner.address.ext', string='Destino') invoices_ids = fields.One2many('account.invoice', 'deposito_operation_id', string='Facturas de Clientes', domain=[('type', '=', 'out_invoice')]) invoice_count = fields.Integer(compute="_compute_invoice", string='Conteo de Facturas', copy=False, default=0, store=True) suppliers_invoices_ids = fields.Many2many('account.invoice', string='Facturas de Proveedor', copy=False) invoice_id = fields.Many2one('account.invoice') @api.multi def draft_confirm(self): return self.write({ 'state': 'confirm', 'name': self.env['ir.sequence'].next_by_code('service.deposito') or '/' }) @api.multi def confirm_inprocess(self): return self.write({'state': 'inprocess'}) @api.multi def confirm_cancel(self): return self.write({'state': 'cancel'}) @api.multi def cancel_draft(self): return self.write({'state': 'draft'}) @api.multi def action_view_invoice(self): ''' This function returns an action that display existing vendor bills of given purchase order ids. When only one found, show the vendor bill immediately. ''' action = self.env.ref('account.action_invoice_tree1') result = action.read()[0] create_bill = self.env.context.get('create_bill', False) # override the context to get rid of the default filtering result['context'] = { 'type': 'out_invoice', 'default_purchase_id': self.id, 'default_currency_id': self.currency_id.id, 'default_company_id': self.company_id.id, 'company_id': self.company_id.id } if not self.invoices_ids: raise Warning('No tiene Facturas creadas aún') # choose the view_mode accordingly if len(self.invoices_ids) > 1 and not create_bill: result['domain'] = "[('id', 'in', " + str( self.invoices_ids.ids) + ")]" else: res = self.env.ref('account.invoice_form', False) result['views'] = [(res and res.id or False, 'form')] # Do not set an invoice_id if we want to create a new bill. if not create_bill: result['res_id'] = self.invoices_ids.id or False result['context']['default_origin'] = self.name # result['context']['default_reference'] = self.partner_ref return result def show_service_lines(self): context = self._context.copy() srv_ids = self.ids act_window = self.env['ir.actions.act_window'] wizard = self # open the list view of service product to invoice res = act_window.for_xml_id('deposito', 'action_consolidado_servicio_tree') # context res['context'] = { 'search_default_uninvoiced': 1, } products_obj = self.env['producto.servicio.camion'] ids_guardadas = [] if self.productos_servicios_camion_ids: for prod in self.productos_servicios_camion_ids: if prod.is_invoiced and not prod.invoiced: ids_guardadas.append(prod.id) if self.cargas_ids: for prod in self.cargas_ids: for cg in prod.producto_servicio_carga_ids: if cg.is_invoiced and not cg.invoiced: ids_guardadas.append(cg.id) # domain if srv_ids: if not res.get('domain', False) or not isinstance( res.get('domain', False), (list, )): res['domain'] = [] # res['domain'].append(('camion_id', 'in', srv_ids)) res['domain'].append(('id', 'in', ids_guardadas)) res['domain'].append(('invoiced', '=', False)) res['domain'].append(('is_invoiced', '=', True)) return res
class ResPartner(models.Model): _name = 'res.partner' _inherit = 'res.partner' _description = 'Partner' @api.multi def _credit_debit_get(self): tables, where_clause, where_params = self.env['account.move.line']._query_get() where_params = [tuple(self.ids)] + where_params self._cr.execute("""SELECT l.partner_id, act.type, SUM(l.amount_residual) FROM account_move_line l LEFT JOIN account_account a ON (l.account_id=a.id) LEFT JOIN account_account_type act ON (a.user_type_id=act.id) WHERE act.type IN ('receivable','payable') AND l.partner_id IN %s AND l.reconciled IS FALSE """ + where_clause + """ GROUP BY l.partner_id, act.type """, where_params) for pid, type, val in self._cr.fetchall(): partner = self.browse(pid) if type == 'receivable': partner.credit = val elif type == 'payable': partner.debit = -val @api.multi def _asset_difference_search(self, account_type, operator, operand): if operator not in ('<', '=', '>', '>=', '<='): return [] if type(operand) not in (float, int): return [] sign = 1 if account_type == 'payable': sign = -1 res = self._cr.execute(''' SELECT partner.id FROM res_partner partner LEFT JOIN account_move_line aml ON aml.partner_id = partner.id RIGHT JOIN account_account acc ON aml.account_id = acc.id WHERE acc.internal_type = %s AND NOT acc.deprecated GROUP BY partner.id HAVING %s * COALESCE(SUM(aml.amount_residual), 0) ''' + operator + ''' %s''', (account_type, sign, operand)) res = self._cr.fetchall() if not res: return [('id', '=', '0')] return [('id', 'in', map(itemgetter(0), res))] @api.model def _credit_search(self, operator, operand): return self._asset_difference_search('receivable', operator, operand) @api.model def _debit_search(self, operator, operand): return self._asset_difference_search('payable', operator, operand) @api.multi def _invoice_total(self): account_invoice_report = self.env['account.invoice.report'] if not self.ids: self.total_invoiced = 0.0 return True user_currency_id = self.env.user.company_id.currency_id.id all_partners_and_children = {} all_partner_ids = [] for partner in self: # price_total is in the company currency all_partners_and_children[partner] = self.search([('id', 'child_of', partner.id)]).ids all_partner_ids += all_partners_and_children[partner] # searching account.invoice.report via the orm is comparatively expensive # (generates queries "id in []" forcing to build the full table). # In simple cases where all invoices are in the same currency than the user's company # access directly these elements # generate where clause to include multicompany rules where_query = account_invoice_report._where_calc([ ('partner_id', 'in', all_partner_ids), ('state', 'not in', ['draft', 'cancel']), ('company_id', '=', self.env.user.company_id.id), ('type', 'in', ('out_invoice', 'out_refund')) ]) account_invoice_report._apply_ir_rules(where_query, 'read') from_clause, where_clause, where_clause_params = where_query.get_sql() # price_total is in the company currency query = """ SELECT SUM(price_total) as total, partner_id FROM account_invoice_report account_invoice_report WHERE %s GROUP BY partner_id """ % where_clause self.env.cr.execute(query, where_clause_params) price_totals = self.env.cr.dictfetchall() for partner, child_ids in all_partners_and_children.items(): partner.total_invoiced = sum(price['total'] for price in price_totals if price['partner_id'] in child_ids) @api.multi def _journal_item_count(self): for partner in self: partner.journal_item_count = self.env['account.move.line'].search_count([('partner_id', '=', partner.id)]) partner.contracts_count = self.env['account.analytic.account'].search_count([('partner_id', '=', partner.id)]) def get_followup_lines_domain(self, date, overdue_only=False, only_unblocked=False): domain = [('reconciled', '=', False), ('account_id.deprecated', '=', False), ('account_id.internal_type', '=', 'receivable'), '|', ('debit', '!=', 0), ('credit', '!=', 0), ('company_id', '=', self.env.user.company_id.id)] if only_unblocked: domain += [('blocked', '=', False)] if self.ids: if 'exclude_given_ids' in self._context: domain += [('partner_id', 'not in', self.ids)] else: domain += [('partner_id', 'in', self.ids)] #adding the overdue lines overdue_domain = ['|', '&', ('date_maturity', '!=', False), ('date_maturity', '<', date), '&', ('date_maturity', '=', False), ('date', '<', date)] if overdue_only: domain += overdue_domain return domain @api.multi def _compute_issued_total(self): """ Returns the issued total as will be displayed on partner view """ today = fields.Date.context_today(self) for partner in self: domain = partner.get_followup_lines_domain(today, overdue_only=True) issued_total = 0 for aml in self.env['account.move.line'].search(domain): issued_total += aml.amount_residual partner.issued_total = issued_total @api.one def _compute_has_unreconciled_entries(self): # Avoid useless work if has_unreconciled_entries is not relevant for this partner if not self.active or not self.is_company and self.parent_id: return self.env.cr.execute( """ SELECT 1 FROM( SELECT p.last_time_entries_checked AS last_time_entries_checked, MAX(l.write_date) AS max_date FROM account_move_line l RIGHT JOIN account_account a ON (a.id = l.account_id) RIGHT JOIN res_partner p ON (l.partner_id = p.id) WHERE p.id = %s AND EXISTS ( SELECT 1 FROM account_move_line l WHERE l.account_id = a.id AND l.partner_id = p.id AND l.amount_residual > 0 ) AND EXISTS ( SELECT 1 FROM account_move_line l WHERE l.account_id = a.id AND l.partner_id = p.id AND l.amount_residual < 0 ) GROUP BY p.last_time_entries_checked ) as s WHERE (last_time_entries_checked IS NULL OR max_date > last_time_entries_checked) """, (self.id,)) self.has_unreconciled_entries = self.env.cr.rowcount == 1 @api.multi def mark_as_reconciled(self): self.env['account.partial.reconcile'].check_access_rights('write') return self.sudo().write({'last_time_entries_checked': time.strftime(DEFAULT_SERVER_DATETIME_FORMAT)}) @api.one def _get_company_currency(self): if self.company_id: self.currency_id = self.sudo().company_id.currency_id else: self.currency_id = self.env.user.company_id.currency_id credit = fields.Monetary(compute='_credit_debit_get', search=_credit_search, string='Total Receivable', help="Total amount this customer owes you.") debit = fields.Monetary(compute='_credit_debit_get', search=_debit_search, string='Total Payable', help="Total amount you have to pay to this vendor.") debit_limit = fields.Monetary('Payable Limit') total_invoiced = fields.Monetary(compute='_invoice_total', string="Total Invoiced", groups='account.group_account_invoice') currency_id = fields.Many2one('res.currency', compute='_get_company_currency', readonly=True, string="Currency", help='Utility field to express amount currency') contracts_count = fields.Integer(compute='_journal_item_count', string="Contracts", type='integer') journal_item_count = fields.Integer(compute='_journal_item_count', string="Journal Items", type="integer") issued_total = fields.Monetary(compute='_compute_issued_total', string="Journal Items") property_account_payable_id = fields.Many2one('account.account', company_dependent=True, string="Account Payable", oldname="property_account_payable", domain="[('internal_type', '=', 'payable'), ('deprecated', '=', False)]", help="This account will be used instead of the default one as the payable account for the current partner", required=True) property_account_receivable_id = fields.Many2one('account.account', company_dependent=True, string="Account Receivable", oldname="property_account_receivable", domain="[('internal_type', '=', 'receivable'), ('deprecated', '=', False)]", help="This account will be used instead of the default one as the receivable account for the current partner", required=True) property_account_position_id = fields.Many2one('account.fiscal.position', company_dependent=True, string="Fiscal Position", help="The fiscal position will determine taxes and accounts used for the partner.", oldname="property_account_position") property_payment_term_id = fields.Many2one('account.payment.term', company_dependent=True, string ='Customer Payment Term', help="This payment term will be used instead of the default one for sale orders and customer invoices", oldname="property_payment_term") property_supplier_payment_term_id = fields.Many2one('account.payment.term', company_dependent=True, string ='Vendor Payment Term', help="This payment term will be used instead of the default one for purchase orders and vendor bills", oldname="property_supplier_payment_term") ref_company_ids = fields.One2many('res.company', 'partner_id', string='Companies that refers to partner', oldname="ref_companies") has_unreconciled_entries = fields.Boolean(compute='_compute_has_unreconciled_entries', help="The partner has at least one unreconciled debit and credit since last time the invoices & payments matching was performed.") last_time_entries_checked = fields.Datetime(oldname='last_reconciliation_date', string='Latest Invoices & Payments Matching Date', readonly=True, copy=False, help='Last time the invoices & payments matching was performed for this partner. ' 'It is set either if there\'s not at least an unreconciled debit and an unreconciled credit ' 'or if you click the "Done" button.') invoice_ids = fields.One2many('account.invoice', 'partner_id', string='Invoices', readonly=True, copy=False) contract_ids = fields.One2many('account.analytic.account', 'partner_id', string='Contracts', readonly=True) bank_account_count = fields.Integer(compute='_compute_bank_count', string="Bank") trust = fields.Selection([('good', 'Good Debtor'), ('normal', 'Normal Debtor'), ('bad', 'Bad Debtor')], string='Degree of trust you have in this debtor', default='normal', company_dependent=True) invoice_warn = fields.Selection(WARNING_MESSAGE, 'Invoice', help=WARNING_HELP, required=True, default="no-message") invoice_warn_msg = fields.Text('Message for Invoice') @api.multi def _compute_bank_count(self): bank_data = self.env['res.partner.bank'].read_group([('partner_id', 'in', self.ids)], ['partner_id'], ['partner_id']) mapped_data = dict([(bank['partner_id'][0], bank['partner_id_count']) for bank in bank_data]) for partner in self: partner.bank_account_count = mapped_data.get(partner.id, 0) def _find_accounting_partner(self, partner): ''' Find the partner for which the accounting entries will be created ''' return partner.commercial_partner_id @api.model def _commercial_fields(self): return super(ResPartner, self)._commercial_fields() + \ ['debit_limit', 'property_account_payable_id', 'property_account_receivable_id', 'property_account_position_id', 'property_payment_term_id', 'property_supplier_payment_term_id', 'last_time_entries_checked']
class SaleOrder(models.Model): _inherit = 'sale.order' @api.depends('order_line.price_total', 'discount', 'chargeable_amount') def _amount_all(self): """ Compute the total amounts of the SO. """ for order in self: amount_untaxed = amount_tax = 0.0 for line in order.order_line: amount_untaxed += line.price_subtotal # FORWARDPORT UP TO 10.0 if order.company_id.tax_calculation_rounding_method == 'round_globally': price = line.price_unit * (1 - (line.discount or 0.0) / 100.0) taxes = line.tax_id.compute_all(price, line.order_id.currency_id, line.product_uom_qty, product=line.product_id, partner=order.partner_shipping_id) amount_tax += sum(t.get('amount', 0.0) for t in taxes.get('taxes', [])) else: amount_tax += line.price_tax amount_total = amount_untaxed + amount_tax if order.chargeable_amount > 0.0: discount = amount_total - order.chargeable_amount else: discount = order.discount amount_total = amount_total - discount round_off_amount = self.env['rounding.off'].round_off_value_to_nearest(amount_total) order.update({ 'amount_untaxed': order.pricelist_id.currency_id.round(amount_untaxed), 'amount_tax': order.pricelist_id.currency_id.round(amount_tax), 'amount_total': amount_total + round_off_amount, 'round_off_amount': round_off_amount, 'total_outstanding_balance': order.prev_outstanding_balance + amount_total + round_off_amount }) @api.depends('partner_id') def _calculate_balance(self): for order in self: order.prev_outstanding_balance = 0.0 order.total_outstanding_balance = 0.0 total_receivable = order._total_receivable() order.prev_outstanding_balance = total_receivable def _total_receivable(self): receivable = 0.0 if self.partner_id: self._cr.execute("""SELECT l.partner_id, at.type, SUM(l.debit-l.credit) FROM account_move_line l LEFT JOIN account_account a ON (l.account_id=a.id) LEFT JOIN account_account_type at ON (a.user_type_id=at.id) WHERE at.type IN ('receivable','payable') AND l.partner_id = %s AND l.full_reconcile_id IS NULL GROUP BY l.partner_id, at.type """, (self.partner_id.id,)) for pid, type, val in self._cr.fetchall(): if val is None: val=0 receivable = (type == 'receivable') and val or -val return receivable @api.depends('partner_id') def _get_partner_details(self): for order in self: partner = order.partner_id order.update({ 'partner_uuid': partner.uuid, #'partner_village': partner.village, }) external_id = fields.Char(string="External Id", help="This field is used to store encounter ID of bahmni api call") dispensed = fields.Boolean(string="Dispensed", help="Flag to identify whether drug order is dispensed or not.") partner_village = fields.Many2one("village.village", string="Partner Village") care_setting = fields.Selection([('ipd', 'IPD'), ('opd', 'OPD')], string="Care Setting") provider_name = fields.Char(string="Provider Name") discount_percentage = fields.Float(string="Discount Percentage") default_quantity = fields.Integer(string="Default Quantity") # above field is used to allow setting quantity as -1 in sale order line, when it is created through bahmni discount_type = fields.Selection([('none', 'No Discount'), ('fixed', 'Fixed'), ('percentage', 'Percentage')], string="Discount Type", default='none') discount = fields.Monetary(string="Discount") disc_acc_id = fields.Many2one('account.account', string="Discount Account Head") round_off_amount = fields.Float(string="Round Off Amount", compute=_amount_all) prev_outstanding_balance = fields.Monetary(string="Previous Outstanding Balance", compute=_calculate_balance) total_outstanding_balance = fields.Monetary(string="Total Outstanding Balance", compute=_amount_all) chargeable_amount = fields.Float(string="Chargeable Amount") amount_round_off = fields.Float(string="Round Off Amount") # location to identify from which location order is placed. location_id = fields.Many2one('stock.location', string="Location") partner_uuid = fields.Char(string='Customer UUID', store=True, readonly=True, compute='_get_partner_details') shop_id = fields.Many2one('sale.shop', 'Shop', required=True) @api.onchange('order_line') def onchange_order_line(self): '''Calculate discount amount, when discount is entered in terms of %''' amount_total = self.amount_untaxed + self.amount_tax if self.discount_type == 'fixed': self.discount_percentage = self.discount/amount_total * 100 elif self.discount_type == 'percentage': self.discount = amount_total * self.discount_percentage / 100 @api.onchange('discount', 'discount_percentage', 'discount_type', 'chargeable_amount') def onchange_discount(self): amount_total = self.amount_untaxed + self.amount_tax if self.chargeable_amount: if self.discount_type == 'none' and self.chargeable_amount: self.discount_type = 'fixed' discount = amount_total - self.chargeable_amount self.discount_percentage = (discount / amount_total) * 100 else: if self.discount: self.discount_percentage = (self.discount / amount_total) * 100 if self.discount_percentage: self.discount = amount_total * self.discount_percentage / 100 @api.model def fields_view_get(self, view_id=None, view_type='form', toolbar=False, submenu=False): '''1. make percentage and discount field readonly, when chargeable amount is allowed to enter''' result = super(SaleOrder, self).fields_view_get(view_id, view_type, toolbar=toolbar, submenu=submenu) if view_type == 'form': group_id = self.env.ref("bahmni_sale.group_allow_change_so_charge").id doc = etree.XML(result['arch']) if group_id in self.env.user.groups_id.ids: for node in doc.xpath("//field[@name='discount_percentage']"): node.set('readonly', '1') setup_modifiers(node, result['fields']['discount_percentage']) for node in doc.xpath("//field[@name='discount']"): node.set('readonly', '1') setup_modifiers(node, result['fields']['discount']) for node in doc.xpath("//field[@name='discount_type']"): node.set('readonly', '1') setup_modifiers(node, result['fields']['discount_type']) result['arch'] = etree.tostring(doc) return result @api.multi def _prepare_invoice(self): """ Prepare the dict of values to create the new invoice for a sales order. This method may be overridden to implement custom invoice generation (making sure to call super() to establish a clean extension chain). """ self.ensure_one() journal_id = self.env['account.invoice'].default_get(['journal_id'])['journal_id'] if not journal_id: raise UserError(_('Please define an accounting sale journal for this company.')) invoice_vals = { 'name': self.client_order_ref or '', 'origin': self.name, 'type': 'out_invoice', 'account_id': self.partner_invoice_id.property_account_receivable_id.id, 'partner_id': self.partner_invoice_id.id, 'partner_shipping_id': self.partner_shipping_id.id, 'journal_id': journal_id, 'currency_id': self.pricelist_id.currency_id.id, 'comment': self.note, 'payment_term_id': self.payment_term_id.id, 'fiscal_position_id': self.fiscal_position_id.id or self.partner_invoice_id.property_account_position_id.id, 'company_id': self.company_id.id, 'user_id': self.user_id and self.user_id.id, 'team_id': self.team_id.id, 'discount_type': self.discount_type, 'discount_percentage': self.discount_percentage, 'disc_acc_id': self.disc_acc_id.id, 'discount': self.discount, } return invoice_vals @api.model def create(self, vals): '''Inherited this method to directly convert quotation to sale order, when it is dispensed at location''' res = super(SaleOrder, self).create(vals) auto_convert_set = self.env['ir.values'].search([('model', '=', 'sale.config.settings'), ('name', '=', 'convert_dispensed')]).value if auto_convert_set and vals.get('dispensed'): # confirm quotation res.action_confirm() # process the delivery order related to this order pickings = self.env['stock.picking'].search([('group_id', '=', res.procurement_group_id.id)]) if res.procurement_group_id else [] for pick in pickings: for ln in pick.pack_operation_product_ids: required_qty = ln.product_qty if ln.product_id.tracking != 'none': # unlinked already populated lot_ids, as in bahmni according to expiry_date assignment is imp. for l in ln.pack_lot_ids: l.unlink() pack_lot_ids = [] alloted_lot_ids = [] while required_qty != 0: lot_id = self.env['stock.production.lot'].search([('product_id', '=', ln.product_id.id), ('life_date', '>', datetime.combine(date.today(), datetime.min.time()).strftime(DSDF)), ('id', 'not in', alloted_lot_ids)], order='life_date', limit=1) if not lot_id: lot_id = self.env['stock.production.lot'].search([('product_id', '=', ln.product_id.id), ('id', 'not in', alloted_lot_ids)], limit=1, order='id') if lot_id: quant_id = self.env['stock.quant'].search([('lot_id', '=', lot_id.id), ('location_id', '=', ln.location_id.id), ('product_id', '=', ln.product_id.id)]) if len(quant_id) == 1: available_qty = quant_id.qty else: available_qty = sum([x.qty for x in quant_id]) if available_qty <= required_qty: pack_lot_ids.append((0, 0, {'lot_id': lot_id.id, 'qty': available_qty, 'qty_todo': available_qty})) required_qty = required_qty - available_qty alloted_lot_ids.append(lot_id.id) elif available_qty > required_qty: pack_lot_ids.append((0, 0, {'lot_id': lot_id.id, 'qty': required_qty, 'qty_todo': required_qty})) required_qty = 0 alloted_lot_ids.append(lot_id.id) ln.pack_lot_ids = pack_lot_ids ln.qty_done = ln.product_qty elif ln.product_id.tracking == 'none': ln.qty_done = ln.product_qty pick.do_new_transfer() # create and process the invoice ctx = {'active_ids': [res.id]} default_vals = self.env['sale.advance.payment.inv' ].with_context(ctx).default_get(['count', 'deposit_taxes_id', 'advance_payment_method', 'product_id', 'deposit_account_id']) payment_inv_wiz = self.env['sale.advance.payment.inv'].with_context(ctx).create(default_vals) payment_inv_wiz.with_context(ctx).create_invoices() for inv in res.invoice_ids: inv.action_invoice_open() account_payment_env = self.env['account.payment'] fields = account_payment_env.fields_get().keys() default_fields = account_payment_env.with_context({'default_invoice_ids': [(4, inv.id, None)]}).default_get(fields) journal_id = self.env['account.journal'].search([('type', '=', 'cash')], limit=1) default_fields.update({'journal_id': journal_id.id}) payment_method_ids = self.env['account.payment.method' ].search([('payment_type', '=', default_fields.get('payment_type'))]).ids if default_fields.get('payment_type') == 'inbound': journal_payment_methods = journal_id.inbound_payment_method_ids.ids elif default_fields.get('payment_type') == 'outbond': journal_payment_methods = journal_id.outbound_payment_method_ids.ids common_payment_method = list(set(payment_method_ids).intersection(set(journal_payment_methods))) common_payment_method.sort() default_fields.update({'payment_method_id': common_payment_method[0]}) account_payment = account_payment_env.create(default_fields) account_payment.post() return res #By Pass the Invoice wizard while we press the "Create Invoice" button in sale order afer confirmation. #So Once we Confirm the sale order it will create the invoice and ask for the register payment. @api.multi def action_confirm(self): res = super(SaleOrder,self).action_confirm() #here we need to set condition for if the its enabled then can continuw owise return True in else condition if self.env.user.has_group('bahmni_sale.group_skip_invoice_options'): for order in self: inv_data = order._prepare_invoice() created_invoice = self.env['account.invoice'].create(inv_data) for line in order.order_line: line.invoice_line_create(created_invoice.id, line.product_uom_qty) # Use additional field helper function (for account extensions) for line in created_invoice.invoice_line_ids: line._set_additional_fields(created_invoice) # Necessary to force computation of taxes. In account_invoice, they are triggered # by onchanges, which are not triggered when doing a create. created_invoice.compute_taxes() created_invoice.message_post_with_view('mail.message_origin_link', values={'self': created_invoice, 'origin': order}, subtype_id=self.env.ref('mail.mt_note').id) created_invoice.action_invoice_open()#Validate Invoice ctx = dict( default_invoice_ids = [(4, created_invoice.id, None)] ) reg_pay_form = self.env.ref('account.view_account_payment_invoice_form') return { 'name': _('Register Payment'), 'type': 'ir.actions.act_window', 'view_type': 'form', 'view_mode': 'form', 'res_model': 'account.payment', 'views': [(reg_pay_form.id, 'form')], 'view_id': reg_pay_form.id, 'target': 'new', 'context': ctx, } else: return res @api.onchange('shop_id') def onchange_shop_id(self): self.warehouse_id = self.shop_id.warehouse_id.id self.location_id = self.shop_id.location_id.id self.payment_term_id = self.shop_id.payment_default_id.id self.project_id = self.shop_id.project_id.id if self.shop_id.project_id else False if self.shop_id.pricelist_id: self.pricelist_id = self.shop_id.pricelist_id.id
class MaterialPurchaseRequisition(models.Model): _name = 'material.purchase.requisition' _description = 'Purchase Requisition' # _inherit = ['mail.thread', 'ir.needaction_mixin'] _inherit = ['mail.thread', 'mail.activity.mixin', 'portal.mixin'] # odoo11 _order = 'id desc' def unlink(self): for rec in self: if rec.state not in ('draft', 'cancel', 'reject'): raise Warning( _('You can not delete Purchase Requisition which is not in draft or cancelled or rejected state.')) return super(MaterialPurchaseRequisition, self).unlink() name = fields.Char( string='Number', index=True, readonly=1, ) state = fields.Selection([ ('draft', 'New'), ('dept_confirm', 'Waiting Department Approval'), ('ir_approve', 'Waiting IR Approved'), ('approve', 'Approved'), ('stock', 'Purchase Order Created'), ('receive', 'Received'), ('cancel', 'Cancelled'), ('reject', 'Rejected')], default='draft', track_visibility='onchange', ) request_date = fields.Date( string='Requisition Date', default=fields.Date.today(), required=True, ) department_id = fields.Many2one( 'hr.department', string='Department', required=True, copy=True, ) employee_id = fields.Many2one( 'hr.employee', string='Employee', default=lambda self: self.env['hr.employee'].search([('user_id', '=', self.env.uid)], limit=1), required=True, copy=True, ) approve_manager_id = fields.Many2one( 'hr.employee', string='Department Manager', readonly=True, copy=False, ) reject_manager_id = fields.Many2one( 'hr.employee', string='Department Manager Reject', readonly=True, ) approve_employee_id = fields.Many2one( 'hr.employee', string='Approved by', readonly=True, copy=False, ) reject_employee_id = fields.Many2one( 'hr.employee', string='Rejected by', readonly=True, copy=False, ) company_id = fields.Many2one( 'res.company', string='Company', default=lambda self: self.env.user.company_id, required=True, copy=True, ) location_id = fields.Many2one( 'stock.location', string='Source Location', copy=True, ) requisition_line_ids = fields.One2many( 'material.purchase.requisition.line', 'requisition_id', string='Purchase Requisitions Line', copy=True, ) date_end = fields.Date( string='Requisition Deadline', readonly=True, help='Last date for the product to be needed', copy=True, ) date_done = fields.Date( string='Date Done', readonly=True, help='Date of Completion of Purchase Requisition', ) managerapp_date = fields.Date( string='Department Approval Date', readonly=True, copy=False, ) manareject_date = fields.Date( string='Department Manager Reject Date', readonly=True, ) userreject_date = fields.Date( string='Rejected Date', readonly=True, copy=False, ) userrapp_date = fields.Date( string='Approved Date', readonly=True, copy=False, ) receive_date = fields.Date( string='Received Date', readonly=True, copy=False, ) reason = fields.Text( string='Reason for Requisitions', required=False, copy=True, ) analytic_account_id = fields.Many2one( 'account.analytic.account', string='Analytic Account', copy=True, ) dest_location_id = fields.Many2one( 'stock.location', string='Destination Location', required=False, copy=True, ) delivery_picking_id = fields.Many2one( 'stock.picking', string='Internal Picking', readonly=True, copy=False, ) requisiton_responsible_id = fields.Many2one( 'hr.employee', string='Requisition Responsible', copy=True, ) employee_confirm_id = fields.Many2one( 'hr.employee', string='Confirmed by', readonly=True, copy=False, ) confirm_date = fields.Date( string='Confirmed Date', readonly=True, copy=False, ) purchase_order_ids = fields.One2many( 'purchase.order', 'custom_requisition_id', string='Purchase Ordes', ) custom_picking_type_id = fields.Many2one( 'stock.picking.type', string='Picking Type', copy=False, ) @api.model def create(self, vals): name = self.env['ir.sequence'].next_by_code('purchase.requisition.seq') vals.update({ 'name': name }) res = super(MaterialPurchaseRequisition, self).create(vals) return res def requisition_confirm(self): for rec in self: manager_mail_template = self.env.ref( 'material_purchase_requisitions.email_confirm_material_purchase_requistion') rec.employee_confirm_id = rec.employee_id.id rec.confirm_date = fields.Date.today() rec.state = 'dept_confirm' if manager_mail_template: manager_mail_template.send_mail(self.id) def requisition_reject(self): for rec in self: rec.state = 'reject' rec.reject_employee_id = self.env['hr.employee'].search([('user_id', '=', self.env.uid)], limit=1) rec.userreject_date = fields.Date.today() def manager_approve(self): for rec in self: rec.managerapp_date = fields.Date.today() rec.approve_manager_id = self.env['hr.employee'].search([('user_id', '=', self.env.uid)], limit=1) employee_mail_template = self.env.ref( 'material_purchase_requisitions.email_purchase_requisition_iruser_custom') email_iruser_template = self.env.ref('material_purchase_requisitions.email_purchase_requisition') employee_mail_template.send_mail(self.id) email_iruser_template.send_mail(self.id) rec.state = 'ir_approve' def user_approve(self): for rec in self: rec.userrapp_date = fields.Date.today() rec.approve_employee_id = self.env['hr.employee'].search([('user_id', '=', self.env.uid)], limit=1) rec.state = 'approve' def reset_draft(self): for rec in self: rec.state = 'draft' def _prepare_pick_vals(self, line=False, stock_id=False): pick_vals = { 'product_id': line.product_id.id, 'product_uom_qty': line.qty, 'product_uom': line.uom.id, 'location_id': self.location_id.id, 'location_dest_id': self.dest_location_id.id, 'name': line.product_id.name, 'picking_type_id': self.custom_picking_type_id.id, 'picking_id': stock_id.id, 'custom_requisition_line_id': line.id } return pick_vals def _prepare_po_line(self, line=False, purchase_order=False): po_line_vals = { 'product_id': line.product_id.id, 'name': line.product_id.name, 'product_qty': line.qty, 'product_uom': line.uom.id, 'date_planned': fields.Date.today(), 'price_unit': line.product_id.lst_price, 'order_id': purchase_order.id, 'account_analytic_id': self.analytic_account_id.id, 'custom_requisition_line_id': line.id } return po_line_vals def request_stock(self): stock_obj = self.env['stock.picking'] move_obj = self.env['stock.move'] # internal_obj = self.env['stock.picking.type'].search([('code','=', 'internal')], limit=1) # internal_obj = self.env['stock.location'].search([('usage','=', 'internal')], limit=1) purchase_obj = self.env['purchase.order'] purchase_line_obj = self.env['purchase.order.line'] # if not internal_obj: # raise UserError(_('Please Specified Internal Picking Type.')) for rec in self: if not rec.requisition_line_ids: raise Warning(_('Please create some requisition lines.')) if any(line.requisition_type == 'internal' for line in rec.requisition_line_ids): if not rec.location_id.id: raise Warning(_('Select Source location under the picking details.')) if not rec.custom_picking_type_id.id: raise Warning(_('Select Picking Type under the picking details.')) if not rec.dest_location_id: raise Warning(_('Select Destination location under the picking details.')) # if not rec.employee_id.dest_location_id.id or not rec.employee_id.department_id.dest_location_id.id: # raise Warning(_('Select Destination location under the picking details.')) picking_vals = { 'partner_id': rec.employee_id.address_home_id.id, 'scheduled_date': fields.Date.today(), 'location_id': rec.location_id.id, 'location_dest_id': rec.dest_location_id and rec.dest_location_id.id or rec.employee_id.dest_location_id.id or rec.employee_id.department_id.dest_location_id.id, 'picking_type_id': rec.custom_picking_type_id.id, # internal_obj.id, 'note': rec.reason, 'custom_requisition_id': rec.id, 'origin': rec.name, } stock_id = stock_obj.sudo().create(picking_vals) delivery_vals = { 'delivery_picking_id': stock_id.id, } rec.write(delivery_vals) po_dict = {} for line in rec.requisition_line_ids: if line.requisition_type == 'internal': pick_vals = rec._prepare_pick_vals(line, stock_id) move_id = move_obj.sudo().create(pick_vals) else: if not line.partner_id: raise Warning(_('PLease Enter Atleast One Vendor on Requisition Lines')) for partner in line.partner_id: if partner not in po_dict: po_vals = { 'partner_id': partner.id, 'currency_id': rec.env.user.company_id.currency_id.id, 'date_order': fields.Date.today(), 'company_id': rec.env.user.company_id.id, 'custom_requisition_id': rec.id, 'origin': rec.name, } purchase_order = purchase_obj.create(po_vals) po_dict.update({partner: purchase_order}) po_line_vals = rec._prepare_po_line(line, purchase_order) # { # 'product_id': line.product_id.id, # 'name':line.product_id.name, # 'product_qty': line.qty, # 'product_uom': line.uom.id, # 'date_planned': fields.Date.today(), # 'price_unit': line.product_id.lst_price, # 'order_id': purchase_order.id, # 'account_analytic_id': rec.analytic_account_id.id, # } purchase_line_obj.sudo().create(po_line_vals) else: purchase_order = po_dict.get(partner) po_line_vals = rec._prepare_po_line(line, purchase_order) # po_line_vals = { # 'product_id': line.product_id.id, # 'name':line.product_id.name, # 'product_qty': line.qty, # 'product_uom': line.uom.id, # 'date_planned': fields.Date.today(), # 'price_unit': line.product_id.lst_price, # 'order_id': purchase_order.id, # 'account_analytic_id': rec.analytic_account_id.id, # } purchase_line_obj.sudo().create(po_line_vals) rec.state = 'stock' def action_received(self): for rec in self: rec.receive_date = fields.Date.today() rec.state = 'receive' def action_cancel(self): for rec in self: rec.state = 'cancel' @api.onchange('employee_id') def set_department(self): for rec in self: rec.department_id = rec.employee_id.department_id.id rec.dest_location_id = rec.employee_id.dest_location_id.id or rec.employee_id.department_id.dest_location_id.id def show_picking(self): for rec in self: res = self.env.ref('stock.action_picking_tree_all') res = res.read()[0] res['domain'] = str([('custom_requisition_id', '=', rec.id)]) return res def action_show_po(self): for rec in self: purchase_action = self.env.ref('purchase.purchase_rfq') purchase_action = purchase_action.read()[0] purchase_action['domain'] = str([('custom_requisition_id', '=', rec.id)]) return purchase_action
class Partner(models.Model): _inherit = 'res.partner' commercial_name = fields.Char(string="Nombre comercial", index=True) registration_name = fields.Char(string="Razón social", related="name", store=True, readonly=False, index=True) l10n_latam_identification_type_id = fields.Many2one(string="Tipo Doc. Id.") vat = fields.Char(string="Nro. Doc. Id.") state = fields.Selection(string="State", selection=[('habido', 'Habido'), ('nhabido', 'No Habido')]) #@api.depends('is_company', 'name', 'parent_id.name', 'type', 'company_name', 'commercial_name') #def _compute_display_name(self) : # super(Partner, self)._compute_display_name() # for partner in self : # partner.display_name = (partner.vat + " - " if partner.vat else "") + partner.display_name + (" - " + partner.commercial_name if partner.commercial_name else "") def _get_name(self): nombre = super(Partner, self)._get_name() #if not self._context.get('show_vat') or not self.vat : # nombre = nombre + (self.vat + " ‒ " if self.vat else "") return nombre def name_get(self): res = super(Partner, self).name_get() #new_res = res[:] #for old_name in res : # new_res.remove(old_name) # new_name = list(old_name) # if isinstance(new_name[0], int) and self.search([('id','=',new_name[0])]) : # new_name.append(self.browse(new_name[0]).vat) # if new_name[2] and (' ‒ ' + new_name[2]) not in new_name[1] : # new_name[1] = new_name[1] + ' ‒ ' + new_name[2] #not the common hyphen # new_name = new_name[:2] # new_res.append(tuple(new_name)) #return new_res return res @api.onchange('l10n_latam_identification_type_id', 'vat') def vat_change(self): if self.l10n_latam_identification_type_id.l10n_pe_vat_code in [ '1', '6' ] and self.vat and self.vat.strip(): self.update_document() def update_document(self): if len( self ) != 1 or not self.l10n_latam_identification_type_id or not self.vat or not self.vat.strip( ): return False cif = self.vat.strip() district_id_default = self.env.ref("l10n_pe.district_pe_" + DEFAULT_ZIPCODE) if self.l10n_latam_identification_type_id.l10n_pe_vat_code == '1': if len(cif) != 8 or not cif.isdigit(): cif = 'El DNI' + ( len(cif) != 8 and ' debe tener 8 caracteres' or '') + ( (len(cif) != 8 and not cif.isdigit()) and ' y ' or '') + ( (not cif.isdigit()) and ' solo debe poseer caracteres numéricos' or '') raise Warning(cif) d = get_data_doc_number("dni", cif, format="json") #d = new_get_data_doc_number("persona", cif, format="json") if d['error']: return True d = d['data'] self.name = '%s %s %s' % (d['nombres'], d['ape_paterno'], d['ape_materno']) self.street_name = False self.country_id = district_id_default.city_id.country_id self.state_id = district_id_default.city_id.state_id self.city_id = district_id_default.city_id self.l10n_pe_district = district_id_default self.zip = district_id_default.code elif self.l10n_latam_identification_type_id.l10n_pe_vat_code == '6': if len(cif) != 11 or not cif.isdigit(): cif = 'El RUC' + ( len(cif) != 11 and ' debe tener 11 caracteres' or '') + ( (len(cif) != 11 and not cif.isdigit()) and ' y' or '') + ( (not cif.isdigit()) and ' solo debe poseer caracteres numéricos' or '') raise Warning(cif) if not self.check_vat_pe(cif): raise Warning('El RUC ingresado no es válido') d = get_data_ruc(cif) #d = get_data_doc_number('ruc', cif, format='json') #d = new_get_data_doc_number('empresa', cif, format='json') if d['error']: return True d = d['data'] city_ids = self.env['res.city'] if 'provincia' in d and d['provincia']: if d['provincia'].strip().upper( ) != district_id_default.city_id.name.upper(): city_ids = city_ids.search([('country_id', '=', 173), ('name', 'ilike', d['provincia'].strip())]) else: city_ids = district_id_default.city_id if city_ids: if 'distrito' in d and d['distrito']: if d['distrito'].strip().upper( ) != district_id_default.name.upper(): district_id_default = self.env[ 'l10n_pe.res.city.district'].search( [('city_id', 'in', city_ids.ids), ('name', 'ilike', d['distrito'].strip())], limit=1) or district_id_default self.country_id = district_id_default.city_id.country_id self.state_id = district_id_default.city_id.state_id self.city_id = district_id_default.city_id self.l10n_pe_district = district_id_default self.zip = district_id_default.code ##d = get_data_doc_number('ruc', cif, format='json') #self.name = d['nombre'].strip() ##self.registration_name = d['nombre'] #related field #self.commercial_name = d['nombre_comercial'].strip() or d['nombre'].strip() #self.street_name = d['domicilio_fiscal'].strip() #self.state = d['condicion_contribuyente'].strip() == 'HABIDO' and 'habido' or 'nhabido' #self.is_company = True ##d = get_data_ruc(cif) self.name = d['razon_social'].strip() #self.registration_name = d['razon_social'] #related field self.commercial_name = d['nombre_comercial'].strip( ) != '-' and d['nombre_comercial'].strip() self.street_name = d['domicilio_fiscal'].strip() self.state = d['contribuyente_condicion'].strip().upper( ) == 'HABIDO' and 'habido' or 'nhabido' self.is_company = True return True return True
class EventEvent(models.Model): """Event""" _name = 'event.event' _description = 'Event' _inherit = ['mail.thread', 'mail.activity.mixin'] _order = 'date_begin' def _get_default_stage_id(self): event_stages = self.env['event.stage'].search([]) return event_stages[0] if event_stages else False def _default_description(self): return self.env['ir.ui.view']._render_template('event.event_default_descripton') name = fields.Char(string='Event', translate=True, required=True) note = fields.Html(string='Note', store=True, compute="_compute_note", readonly=False) description = fields.Html(string='Description', translate=html_translate, sanitize_attributes=False, sanitize_form=False, default=_default_description) active = fields.Boolean(default=True) user_id = fields.Many2one( 'res.users', string='Responsible', tracking=True, default=lambda self: self.env.user) company_id = fields.Many2one( 'res.company', string='Company', change_default=True, default=lambda self: self.env.company, required=False) organizer_id = fields.Many2one( 'res.partner', string='Organizer', tracking=True, default=lambda self: self.env.company.partner_id, domain="['|', ('company_id', '=', False), ('company_id', '=', company_id)]") event_type_id = fields.Many2one('event.type', string='Template', ondelete='set null') event_mail_ids = fields.One2many( 'event.mail', 'event_id', string='Mail Schedule', copy=True, compute='_compute_event_mail_ids', readonly=False, store=True) tag_ids = fields.Many2many( 'event.tag', string="Tags", readonly=False, store=True, compute="_compute_tag_ids") # Kanban fields kanban_state = fields.Selection([('normal', 'In Progress'), ('done', 'Done'), ('blocked', 'Blocked')], default='normal', copy=False) kanban_state_label = fields.Char( string='Kanban State Label', compute='_compute_kanban_state_label', store=True, tracking=True) stage_id = fields.Many2one( 'event.stage', ondelete='restrict', default=_get_default_stage_id, group_expand='_read_group_stage_ids', tracking=True, copy=False) legend_blocked = fields.Char(related='stage_id.legend_blocked', string='Kanban Blocked Explanation', readonly=True) legend_done = fields.Char(related='stage_id.legend_done', string='Kanban Valid Explanation', readonly=True) legend_normal = fields.Char(related='stage_id.legend_normal', string='Kanban Ongoing Explanation', readonly=True) # Seats and computation seats_max = fields.Integer( string='Maximum Attendees Number', compute='_compute_seats_max', readonly=False, store=True, help="For each event you can define a maximum registration of seats(number of attendees), above this numbers the registrations are not accepted.") seats_limited = fields.Boolean('Maximum Attendees', required=True, compute='_compute_seats_limited', readonly=False, store=True) seats_reserved = fields.Integer( string='Reserved Seats', store=True, readonly=True, compute='_compute_seats') seats_available = fields.Integer( string='Available Seats', store=True, readonly=True, compute='_compute_seats') seats_unconfirmed = fields.Integer( string='Unconfirmed Seat Reservations', store=True, readonly=True, compute='_compute_seats') seats_used = fields.Integer( string='Number of Participants', store=True, readonly=True, compute='_compute_seats') seats_expected = fields.Integer( string='Number of Expected Attendees', compute_sudo=True, readonly=True, compute='_compute_seats_expected') # Registration fields auto_confirm = fields.Boolean( string='Autoconfirmation', compute='_compute_auto_confirm', readonly=False, store=True, help='Autoconfirm Registrations. Registrations will automatically be confirmed upon creation.') registration_ids = fields.One2many('event.registration', 'event_id', string='Attendees') event_ticket_ids = fields.One2many( 'event.event.ticket', 'event_id', string='Event Ticket', copy=True, compute='_compute_event_ticket_ids', readonly=False, store=True) event_registrations_started = fields.Boolean( 'Registrations started', compute='_compute_event_registrations_started', help="registrations have started if the current datetime is after the earliest starting date of tickets." ) event_registrations_open = fields.Boolean( 'Registration open', compute='_compute_event_registrations_open', compute_sudo=True, help="Registrations are open if:\n" "- the event is not ended\n" "- there are seats available on event\n" "- the tickets are sellable (if ticketing is used)") event_registrations_sold_out = fields.Boolean( 'Sold Out', compute='_compute_event_registrations_sold_out', compute_sudo=True, help='The event is sold out if no more seats are available on event. If ticketing is used and all tickets are sold out, the event will be sold out.') start_sale_datetime = fields.Datetime( 'Start sale date', compute='_compute_start_sale_date', help='If ticketing is used, contains the earliest starting sale date of tickets.') # Date fields date_tz = fields.Selection( _tz_get, string='Timezone', required=True, compute='_compute_date_tz', readonly=False, store=True) date_begin = fields.Datetime(string='Start Date', required=True, tracking=True) date_end = fields.Datetime(string='End Date', required=True, tracking=True) date_begin_located = fields.Char(string='Start Date Located', compute='_compute_date_begin_tz') date_end_located = fields.Char(string='End Date Located', compute='_compute_date_end_tz') is_ongoing = fields.Boolean('Is Ongoing', compute='_compute_is_ongoing', search='_search_is_ongoing') is_one_day = fields.Boolean(compute='_compute_field_is_one_day') # Location and communication address_id = fields.Many2one( 'res.partner', string='Venue', default=lambda self: self.env.company.partner_id.id, tracking=True, domain="['|', ('company_id', '=', False), ('company_id', '=', company_id)]") country_id = fields.Many2one( 'res.country', 'Country', related='address_id.country_id', readonly=False, store=True) # badge fields badge_front = fields.Html(string='Badge Front') badge_back = fields.Html(string='Badge Back') badge_innerleft = fields.Html(string='Badge Inner Left') badge_innerright = fields.Html(string='Badge Inner Right') event_logo = fields.Html(string='Event Logo') @api.depends('stage_id', 'kanban_state') def _compute_kanban_state_label(self): for event in self: if event.kanban_state == 'normal': event.kanban_state_label = event.stage_id.legend_normal elif event.kanban_state == 'blocked': event.kanban_state_label = event.stage_id.legend_blocked else: event.kanban_state_label = event.stage_id.legend_done @api.depends('seats_max', 'registration_ids.state') def _compute_seats(self): """ Determine reserved, available, reserved but unconfirmed and used seats. """ # initialize fields to 0 for event in self: event.seats_unconfirmed = event.seats_reserved = event.seats_used = event.seats_available = 0 # aggregate registrations by event and by state state_field = { 'draft': 'seats_unconfirmed', 'open': 'seats_reserved', 'done': 'seats_used', } base_vals = dict((fname, 0) for fname in state_field.values()) results = dict((event_id, dict(base_vals)) for event_id in self.ids) if self.ids: query = """ SELECT event_id, state, count(event_id) FROM event_registration WHERE event_id IN %s AND state IN ('draft', 'open', 'done') GROUP BY event_id, state """ self.env['event.registration'].flush(['event_id', 'state']) self._cr.execute(query, (tuple(self.ids),)) res = self._cr.fetchall() for event_id, state, num in res: results[event_id][state_field[state]] += num # compute seats_available for event in self: event.update(results.get(event._origin.id or event.id, base_vals)) if event.seats_max > 0: event.seats_available = event.seats_max - (event.seats_reserved + event.seats_used) @api.depends('seats_unconfirmed', 'seats_reserved', 'seats_used') def _compute_seats_expected(self): for event in self: event.seats_expected = event.seats_unconfirmed + event.seats_reserved + event.seats_used @api.depends('date_tz', 'start_sale_datetime') def _compute_event_registrations_started(self): for event in self: event = event._set_tz_context() if event.start_sale_datetime: current_datetime = fields.Datetime.context_timestamp(event, fields.Datetime.now()) start_sale_datetime = fields.Datetime.context_timestamp(event, event.start_sale_datetime) event.event_registrations_started = (current_datetime >= start_sale_datetime) else: event.event_registrations_started = True @api.depends('date_tz', 'event_registrations_started', 'date_end', 'seats_available', 'seats_limited', 'event_ticket_ids.sale_available') def _compute_event_registrations_open(self): """ Compute whether people may take registrations for this event * event.date_end -> if event is done, registrations are not open anymore; * event.start_sale_datetime -> lowest start date of tickets (if any; start_sale_datetime is False if no ticket are defined, see _compute_start_sale_date); * any ticket is available for sale (seats available) if any; * seats are unlimited or seats are available; """ for event in self: event = event._set_tz_context() current_datetime = fields.Datetime.context_timestamp(event, fields.Datetime.now()) date_end_tz = event.date_end.astimezone(pytz.timezone(event.date_tz or 'UTC')) if event.date_end else False event.event_registrations_open = event.event_registrations_started and \ (date_end_tz >= current_datetime if date_end_tz else True) and \ (not event.seats_limited or event.seats_available) and \ (not event.event_ticket_ids or any(ticket.sale_available for ticket in event.event_ticket_ids)) @api.depends('event_ticket_ids.start_sale_datetime') def _compute_start_sale_date(self): """ Compute the start sale date of an event. Currently lowest starting sale date of tickets if they are used, of False. """ for event in self: start_dates = [ticket.start_sale_datetime for ticket in event.event_ticket_ids if not ticket.is_expired] event.start_sale_datetime = min(start_dates) if start_dates and all(start_dates) else False @api.depends('event_ticket_ids.sale_available') def _compute_event_registrations_sold_out(self): for event in self: if event.seats_limited and not event.seats_available: event.event_registrations_sold_out = True elif event.event_ticket_ids: event.event_registrations_sold_out = not any( ticket.seats_available > 0 if ticket.seats_limited else True for ticket in event.event_ticket_ids ) else: event.event_registrations_sold_out = False @api.depends('date_tz', 'date_begin') def _compute_date_begin_tz(self): for event in self: if event.date_begin: event.date_begin_located = format_datetime( self.env, event.date_begin, tz=event.date_tz, dt_format='medium') else: event.date_begin_located = False @api.depends('date_tz', 'date_end') def _compute_date_end_tz(self): for event in self: if event.date_end: event.date_end_located = format_datetime( self.env, event.date_end, tz=event.date_tz, dt_format='medium') else: event.date_end_located = False @api.depends('date_begin', 'date_end') def _compute_is_ongoing(self): now = fields.Datetime.now() for event in self: event.is_ongoing = event.date_begin <= now < event.date_end def _search_is_ongoing(self, operator, value): if operator not in ['=', '!=']: raise ValueError(_('This operator is not supported')) if not isinstance(value, bool): raise ValueError(_('Value should be True or False (not %s)'), value) now = fields.Datetime.now() if (operator == '=' and value) or (operator == '!=' and not value): domain = [('date_begin', '<=', now), ('date_end', '>', now)] else: domain = ['|', ('date_begin', '>', now), ('date_end', '<=', now)] event_ids = self.env['event.event']._search(domain) return [('id', 'in', event_ids)] @api.depends('date_begin', 'date_end', 'date_tz') def _compute_field_is_one_day(self): for event in self: # Need to localize because it could begin late and finish early in # another timezone event = event._set_tz_context() begin_tz = fields.Datetime.context_timestamp(event, event.date_begin) end_tz = fields.Datetime.context_timestamp(event, event.date_end) event.is_one_day = (begin_tz.date() == end_tz.date()) @api.depends('event_type_id') def _compute_date_tz(self): for event in self: if event.event_type_id.default_timezone: event.date_tz = event.event_type_id.default_timezone if not event.date_tz: event.date_tz = self.env.user.tz or 'UTC' # seats @api.depends('event_type_id') def _compute_seats_max(self): """ Update event configuration from its event type. Depends are set only on event_type_id itself, not its sub fields. Purpose is to emulate an onchange: if event type is changed, update event configuration. Changing event type content itself should not trigger this method. """ for event in self: if not event.event_type_id: event.seats_max = event.seats_max or 0 else: event.seats_max = event.event_type_id.seats_max or 0 @api.depends('event_type_id') def _compute_seats_limited(self): """ Update event configuration from its event type. Depends are set only on event_type_id itself, not its sub fields. Purpose is to emulate an onchange: if event type is changed, update event configuration. Changing event type content itself should not trigger this method. """ for event in self: if event.event_type_id.has_seats_limitation != event.seats_limited: event.seats_limited = event.event_type_id.has_seats_limitation if not event.seats_limited: event.seats_limited = False @api.depends('event_type_id') def _compute_auto_confirm(self): """ Update event configuration from its event type. Depends are set only on event_type_id itself, not its sub fields. Purpose is to emulate an onchange: if event type is changed, update event configuration. Changing event type content itself should not trigger this method. """ for event in self: event.auto_confirm = event.event_type_id.auto_confirm @api.depends('event_type_id') def _compute_event_mail_ids(self): """ Update event configuration from its event type. Depends are set only on event_type_id itself, not its sub fields. Purpose is to emulate an onchange: if event type is changed, update event configuration. Changing event type content itself should not trigger this method. When synchronizing mails: * lines that are not sent and have no registrations linked are remove; * type lines are added; """ for event in self: if not event.event_type_id and not event.event_mail_ids: event.event_mail_ids = False continue # lines to keep: those with already sent emails or registrations mails_to_remove = event.event_mail_ids.filtered( lambda mail: not(mail._origin.mail_done) and not(mail._origin.mail_registration_ids) ) command = [Command.unlink(mail.id) for mail in mails_to_remove] if event.event_type_id.event_type_mail_ids: command += [ Command.create({ attribute_name: line[attribute_name] if not isinstance(line[attribute_name], models.BaseModel) else line[attribute_name].id for attribute_name in self.env['event.type.mail']._get_event_mail_fields_whitelist() }) for line in event.event_type_id.event_type_mail_ids ] if command: event.event_mail_ids = command @api.depends('event_type_id') def _compute_tag_ids(self): """ Update event configuration from its event type. Depends are set only on event_type_id itself, not its sub fields. Purpose is to emulate an onchange: if event type is changed, update event configuration. Changing event type content itself should not trigger this method. """ for event in self: if not event.tag_ids and event.event_type_id.tag_ids: event.tag_ids = event.event_type_id.tag_ids @api.depends('event_type_id') def _compute_event_ticket_ids(self): """ Update event configuration from its event type. Depends are set only on event_type_id itself, not its sub fields. Purpose is to emulate an onchange: if event type is changed, update event configuration. Changing event type content itself should not trigger this method. When synchronizing tickets: * lines that have no registrations linked are remove; * type lines are added; Note that updating event_ticket_ids triggers _compute_start_sale_date (start_sale_datetime computation) so ensure result to avoid cache miss. """ for event in self: if not event.event_type_id and not event.event_ticket_ids: event.event_ticket_ids = False continue # lines to keep: those with existing registrations tickets_to_remove = event.event_ticket_ids.filtered(lambda ticket: not ticket._origin.registration_ids) command = [Command.unlink(ticket.id) for ticket in tickets_to_remove] if event.event_type_id.event_type_ticket_ids: command += [ Command.create({ attribute_name: line[attribute_name] if not isinstance(line[attribute_name], models.BaseModel) else line[attribute_name].id for attribute_name in self.env['event.type.ticket']._get_event_ticket_fields_whitelist() }) for line in event.event_type_id.event_type_ticket_ids ] event.event_ticket_ids = command @api.depends('event_type_id') def _compute_note(self): for event in self: if event.event_type_id and not is_html_empty(event.event_type_id.note): event.note = event.event_type_id.note @api.constrains('seats_max', 'seats_available', 'seats_limited') def _check_seats_limit(self): if any(event.seats_limited and event.seats_max and event.seats_available < 0 for event in self): raise ValidationError(_('No more available seats.')) @api.constrains('date_begin', 'date_end') def _check_closing_date(self): for event in self: if event.date_end < event.date_begin: raise ValidationError(_('The closing date cannot be earlier than the beginning date.')) @api.model def _read_group_stage_ids(self, stages, domain, order): return self.env['event.stage'].search([]) @api.model_create_multi def create(self, vals_list): for vals in vals_list: # Temporary fix for ``seats_limited`` and ``date_tz`` required fields vals.update(self._sync_required_computed(vals)) events = super(EventEvent, self).create(vals_list) for res in events: if res.organizer_id: res.message_subscribe([res.organizer_id.id]) events.flush() return events def write(self, vals): if 'stage_id' in vals and 'kanban_state' not in vals: # reset kanban state when changing stage vals['kanban_state'] = 'normal' res = super(EventEvent, self).write(vals) if vals.get('organizer_id'): self.message_subscribe([vals['organizer_id']]) return res @api.returns('self', lambda value: value.id) def copy(self, default=None): self.ensure_one() default = dict(default or {}, name=_("%s (copy)") % (self.name)) return super(EventEvent, self).copy(default) @api.model def _get_mail_message_access(self, res_ids, operation, model_name=None): if ( operation == 'create' and self.env.user.has_group('event.group_event_registration_desk') and (not model_name or model_name == 'event.event') ): # allow the registration desk users to post messages on Event # can not be done with "_mail_post_access" otherwise public user will be # able to post on published Event (see website_event) return 'read' return super(EventEvent, self)._get_mail_message_access(res_ids, operation, model_name) def _sync_required_computed(self, values): # TODO: See if the change to seats_limited affects this ? """ Call compute fields in cache to find missing values for required fields (seats_limited and date_tz) in case they are not given in values """ missing_fields = list(set(['seats_limited', 'date_tz']).difference(set(values.keys()))) if missing_fields and values: cache_event = self.new(values) cache_event._compute_seats_limited() cache_event._compute_date_tz() return dict((fname, cache_event[fname]) for fname in missing_fields) else: return {} def _set_tz_context(self): self.ensure_one() return self.with_context(tz=self.date_tz or 'UTC') def action_set_done(self): """ Action which will move the events into the first next (by sequence) stage defined as "Ended" (if they are not already in an ended stage) """ first_ended_stage = self.env['event.stage'].search([('pipe_end', '=', True)], order='sequence') if first_ended_stage: self.write({'stage_id': first_ended_stage[0].id}) def mail_attendees(self, template_id, force_send=False, filter_func=lambda self: self.state != 'cancel'): for event in self: for attendee in event.registration_ids.filtered(filter_func): self.env['mail.template'].browse(template_id).send_mail(attendee.id, force_send=force_send) def _get_ics_file(self): """ Returns iCalendar file for the event invitation. :returns a dict of .ics file content for each event """ result = {} if not vobject: return result for event in self: cal = vobject.iCalendar() cal_event = cal.add('vevent') cal_event.add('created').value = fields.Datetime.now().replace(tzinfo=pytz.timezone('UTC')) cal_event.add('dtstart').value = fields.Datetime.from_string(event.date_begin).replace(tzinfo=pytz.timezone('UTC')) cal_event.add('dtend').value = fields.Datetime.from_string(event.date_end).replace(tzinfo=pytz.timezone('UTC')) cal_event.add('summary').value = event.name if event.address_id: cal_event.add('location').value = event.sudo().address_id.contact_address result[event.id] = cal.serialize().encode('utf-8') return result @api.autovacuum def _gc_mark_events_done(self): """ move every ended events in the next 'ended stage' """ ended_events = self.env['event.event'].search([ ('date_end', '<', fields.Datetime.now()), ('stage_id.pipe_end', '=', False), ]) if ended_events: ended_events.action_set_done()