def unlink(self): """Override to: - Clean up the variants that use any of the values in self: - Remove the value from the variant if the value belonged to an attribute line with only one value. - Unlink or archive all related variants. - Archive the value if unlink is not possible. Archiving is typically needed when the value is referenced elsewhere (on a variant that can't be deleted, on a sales order line, ...). """ # Directly remove the values from the variants for lines that had single # value (counting also the values that are archived). single_values = self.filtered(lambda ptav: len(ptav.attribute_line_id.product_template_value_ids) == 1) for ptav in single_values: ptav.ptav_product_variant_ids.write({'product_template_attribute_value_ids': [(3, ptav.id, 0)]}) # Try to remove the variants before deleting to potentially remove some # blocking references. self.ptav_product_variant_ids._unlink_or_archive() # Now delete or archive the values. ptav_to_archive = self.env['product.template.attribute.value'] for ptav in self: try: with self.env.cr.savepoint(), tools.mute_logger('coffice.sql_db'): super(ProductTemplateAttributeLine, ptav).unlink() except Exception: # We catch all kind of exceptions to be sure that the operation # doesn't fail. ptav_to_archive += ptav ptav_to_archive.write({'ptav_active': False}) return True
def _procure_calculation_orderpoint(self): with api.Environment.manage(): # As this function is in a new thread, I need to open a new cursor, because the old one may be closed new_cr = self.pool.cursor() self = self.with_env(self.env(cr=new_cr)) scheduler_cron = self.sudo().env.ref( 'stock.ir_cron_scheduler_action') # Avoid to run the scheduler multiple times in the same time try: with tools.mute_logger('coffice.sql_db'): self._cr.execute( "SELECT id FROM ir_cron WHERE id = %s FOR UPDATE NOWAIT", (scheduler_cron.id, )) except Exception: _logger.info( 'Attempt to run procurement scheduler aborted, as already running' ) self._cr.rollback() self._cr.close() return {} for company in self.env.user.company_ids: cids = (self.env.user.company_id | self.env.user.company_ids).ids self.env['procurement.group'].with_context( allowed_company_ids=cids).run_scheduler( use_new_cursor=self._cr.dbname, company_id=company.id) new_cr.close() return {}
def unlink(self): """Override to: - Archive the line if unlink is not possible. - Clean up related values and related variants. Archiving is typically needed when the line has values that can't be deleted because they are referenced elsewhere (on a variant that can't be deleted, on a sales order line, ...). """ # Try to remove the values first to remove some potentially blocking # references, which typically works: # - For single value lines because the values are directly removed from # the variants. # - For values that are present on variants that can be deleted. self.product_template_value_ids._only_active().unlink() # Keep a reference to the related templates before the deletion. templates = self.product_tmpl_id # Now delete or archive the lines. ptal_to_archive = self.env['product.template.attribute.line'] for ptal in self: try: with self.env.cr.savepoint(), tools.mute_logger('coffice.sql_db'): super(ProductTemplateAttributeLine, ptal).unlink() except Exception: # We catch all kind of exceptions to be sure that the operation # doesn't fail. ptal_to_archive += ptal ptal_to_archive.write({'active': False}) # For archived lines `_update_product_template_attribute_values` is # implicitly called during the `write` above, but for products that used # unlinked lines `_create_variant_ids` has to be called manually. (templates - ptal_to_archive.product_tmpl_id)._create_variant_ids() return True
def _unlink_or_archive(self, check_access=True): """Unlink or archive products. Try in batch as much as possible because it is much faster. Use dichotomy when an exception occurs. """ # Avoid access errors in case the products is shared amongst companies # but the underlying objects are not. If unlink fails because of an # AccessError (e.g. while recomputing fields), the 'write' call will # fail as well for the same reason since the field has been set to # recompute. if check_access: self.check_access_rights('unlink') self.check_access_rule('unlink') self.check_access_rights('write') self.check_access_rule('write') self = self.sudo() try: with self.env.cr.savepoint(), tools.mute_logger('coffice.sql_db'): self.unlink() except Exception: # We catch all kind of exceptions to be sure that the operation # doesn't fail. if len(self) > 1: self[:len(self) // 2]._unlink_or_archive(check_access=False) self[len(self) // 2:]._unlink_or_archive(check_access=False) else: if self.active: # Note: this can still fail if something is preventing # from archiving. # This is the case from existing stock reordering rules. self.write({'active': False})
def test_sale_transaction_mismatch(self): """Test that a transaction for the incorrect amount does not validate the SO.""" # modify order total self.order.order_line[0].price_unit = 200.0 self.transaction._set_transaction_done() with mute_logger('coffice.addons.sale.models.payment'): self.transaction._post_process_after_done() self.assertEqual( self.order.state, 'draft', 'a transaction for an incorrect amount should not validate a quote' )
def test_remove_badge_with_give_badge(self): self.certification_survey.write({ 'certification_give_badge': True, 'certification_badge_id': self.certification_badge.id }) with mute_logger('coffice.sql_db'): with self.assertRaises(IntegrityError): self.certification_survey.write( {'certification_badge_id': None}) self.certification_survey.flush(['certification_badge_id'])
def test_sql_constraint_dates(self): # The goal is mainly to verify that a human friendly # error message is triggered if the date_from is after # date_to. Coming from a bug due to the new ORM 13.0 leave_vals = { 'name': 'Sick Time Off', 'holiday_status_id': self.env.ref('hr_holidays.holiday_status_cl').id, 'date_from': datetime.today().strftime('%Y-%m-11 19:00:00'), 'date_to': datetime.today().strftime('%Y-%m-10 10:00:00'), 'employee_id': self.ref('hr.employee_admin'), 'number_of_days': 1, } with mute_logger('coffice.sql_db'): with self.assertRaises(IntegrityError): with self.cr.savepoint(): self.env['hr.leave'].create(leave_vals) leave_vals = { 'name': 'Sick Time Off', 'holiday_status_id': self.env.ref('hr_holidays.holiday_status_cl').id, 'date_from': datetime.today().strftime('%Y-%m-10 10:00:00'), 'date_to': datetime.today().strftime('%Y-%m-11 19:00:00'), 'employee_id': self.ref('hr.employee_admin'), 'number_of_days': 1, } leave = self.env['hr.leave'].create(leave_vals) with mute_logger('coffice.sql_db'): with self.assertRaises(IntegrityError): # No ValidationError with self.cr.savepoint(): leave.write({ 'date_from': datetime.today().strftime('%Y-%m-11 19:00:00'), 'date_to': datetime.today().strftime('%Y-%m-10 10:00:00'), })
def update_records(model, src, field_model='model', field_id='res_id'): Model = self.env[model] if model in self.env else None if Model is None: return records = Model.sudo().search([(field_model, '=', 'res.partner'), (field_id, '=', src.id)]) try: with mute_logger('coffice.sql_db'), self._cr.savepoint( ), self.env.clear_upon_failure(): records.sudo().write({field_id: dst_partner.id}) records.flush() except psycopg2.Error: # updating fails, most likely due to a violated unique constraint # keeping record with nonexistent partner_id is useless, better delete it records.sudo().unlink()
def test_set_same_badge_on_multiple_survey(self): self.certification_survey.write({ 'certification_give_badge': True, 'certification_badge_id': self.certification_badge.id }) # set the same badge on another survey should fail: with mute_logger('coffice.sql_db'): with self.assertRaises(IntegrityError): self.certification_survey_2.write({ 'certification_give_badge': True, 'certification_badge_id': self.certification_badge.id }) self.certification_survey.flush()
def test_remove_badge_with_give_badge_multi(self): self.certification_survey.write({ 'certification_give_badge': True, 'certification_badge_id': self.certification_badge.id }) self.certification_survey_2.write({ 'certification_give_badge': True, 'certification_badge_id': self.certification_badge_2.id }) surveys = self.env['survey.survey'].browse( [self.certification_survey.id, self.certification_survey_2.id]) with mute_logger('coffice.sql_db'): with self.assertRaises(IntegrityError): surveys.write({'certification_badge_id': None}) surveys.flush(['certification_badge_id'])
def test_105_duplicated_translation(self): """ Test synchronizing translations with duplicated source """ # create a category with a French translation padawans = self.env['res.partner.category'].create({'name': 'Padawan'}) self.env['ir.translation'].create({ 'type': 'model', 'name': 'res.partner.category,name', 'module': 'base', 'lang': 'fr_FR', 'res_id': padawans.id, 'value': 'Apprenti', 'state': 'translated', }) # change name and insert a duplicate manually padawans.write({'name': 'Padawans'}) with self.assertRaises(IntegrityError), mute_logger('coffice.sql_db'): with self.env.cr.savepoint(): self.env['ir.translation'].create({ 'type': 'model', 'name': 'res.partner.category,name', 'module': 'base', 'lang': 'fr_FR', 'res_id': padawans.id, 'value': 'Apprentis', 'state': 'translated', }) self.env['ir.translation'].translate_fields('res.partner.category', padawans.id, 'name') translations = self.env['ir.translation'].search([ ('res_id', '=', padawans.id), ('name', '=', 'res.partner.category,name'), ('lang', '=', 'fr_FR'), ]) self.assertEqual( len(translations), 1, "Translations were not duplicated after `translate_fields` call") self.assertEqual(translations.value, "Apprenti", "The first translation must stay")
def update(self, inactivity_period): """ Updates the last_poll and last_presence of the current user :param inactivity_period: duration in milliseconds """ presence = self.search([('user_id', '=', self._uid)], limit=1) # compute last_presence timestamp last_presence = datetime.datetime.now() - datetime.timedelta( milliseconds=inactivity_period) values = { 'last_poll': time.strftime(DEFAULT_SERVER_DATETIME_FORMAT), } # update the presence or a create a new one if not presence: # create a new presence for the user values['user_id'] = self._uid values['last_presence'] = last_presence self.create(values) else: # update the last_presence if necessary, and write values if presence.last_presence < last_presence: values['last_presence'] = last_presence # Hide transaction serialization errors, which can be ignored, the presence update is not essential with tools.mute_logger('coffice.sql_db'): presence.write(values) # avoid TransactionRollbackError self.env.cr.commit() # TODO : check if still necessary
def test_sale_mrp(self): warehouse0 = self.env.ref('stock.warehouse0') # In order to test the sale_mrp module in OpenERP, I start by creating a new product 'Slider Mobile' # I define product category Mobile Products Sellable. with mute_logger('coffice.tests.common.onchange'): # Suppress warning on "Changing your cost method" when creating a # product category pc = Form(self.env['product.category']) pc.name = 'Mobile Products Sellable' product_category_allproductssellable0 = pc.save() uom_unit = self.env.ref('uom.product_uom_unit') self.assertIn("seller_ids", self.env['product.template'].fields_get()) # I define product for Slider Mobile. product = Form(self.env['product.template']) product.categ_id = product_category_allproductssellable0 product.list_price = 200.0 product.name = 'Slider Mobile' product.type = 'product' product.uom_id = uom_unit product.uom_po_id = uom_unit product.route_ids.clear() product.route_ids.add(warehouse0.manufacture_pull_id.route_id) product.route_ids.add(warehouse0.mto_pull_id.route_id) product_template_slidermobile0 = product.save() std_price_wiz = Form( self.env['stock.change.standard.price'].with_context( active_id=product_template_slidermobile0.id, active_model='product.template')) std_price_wiz.new_price = 189 std_price_wiz.save() product_component = Form(self.env['product.product']) product_component.name = 'Battery' product_product_bettery = product_component.save() with Form(self.env['mrp.bom']) as bom: bom.product_tmpl_id = product_template_slidermobile0 with bom.bom_line_ids.new() as line: line.product_id = product_product_bettery line.product_qty = 4 # I create a sale order for product Slider mobile so_form = Form(self.env['sale.order']) so_form.partner_id = self.env.ref('base.res_partner_4') with so_form.order_line.new() as line: line.product_id = product_template_slidermobile0.product_variant_ids line.price_unit = 200 line.product_uom_qty = 500.0 line.customer_lead = 7.0 sale_order_so0 = so_form.save() # I confirm the sale order sale_order_so0.action_confirm() # I verify that a manufacturing order has been generated, and that its name and reference are correct mo = self.env['mrp.production'].search( [('origin', 'like', sale_order_so0.name)], limit=1) self.assertTrue(mo, 'Manufacturing order has not been generated')
def test_constraints(self): """The goal of this test is to make sure constraints are correct.""" with self.assertRaises( UserError, msg= "can't change variants creation mode of attribute used on product" ): self.ram_attribute.create_variant = 'no_variant' with self.assertRaises(UserError, msg="can't delete attribute used on product"): self.ram_attribute.unlink() with self.assertRaises( UserError, msg="can't change the attribute of an value used on product"): self.ram_32.attribute_id = self.hdd_attribute.id with self.assertRaises(UserError, msg="can't delete value used on product"): self.ram_32.unlink() with self.assertRaises( ValidationError, msg="can't have attribute without value on product"): self.env['product.template.attribute.line'].create({ 'product_tmpl_id': self.computer_case.id, 'attribute_id': self.hdd_attribute.id, 'value_ids': [(6, 0, [])], }) with self.assertRaises( ValidationError, msg="value attribute must match line attribute"): self.env['product.template.attribute.line'].create({ 'product_tmpl_id': self.computer_case.id, 'attribute_id': self.ram_attribute.id, 'value_ids': [(6, 0, [self.ssd_256.id])], }) with self.assertRaises( UserError, msg="can't change the attribute of an attribute line"): self.computer_ssd_attribute_lines.attribute_id = self.hdd_attribute.id with self.assertRaises( UserError, msg="can't change the product of an attribute line"): self.computer_ssd_attribute_lines.product_tmpl_id = self.computer_case.id with self.assertRaises( UserError, msg= "can't change the value of a product template attribute value" ): self.computer_ram_attribute_lines.product_template_value_ids[ 0].product_attribute_value_id = self.hdd_1 with self.assertRaises( UserError, msg= "can't change the product of a product template attribute value" ): self.computer_ram_attribute_lines.product_template_value_ids[ 0].product_tmpl_id = self.computer_case.id with mute_logger('coffice.sql_db'), self.assertRaises( IntegrityError, msg= "can't have two values with the same name for the same attribute" ): self.env['product.attribute.value'].create({ 'name': '32 GB', 'attribute_id': self.ram_attribute.id, })
def test_rename_unique(self): """ one cannot create two fields with the same name on a given model """ field1 = self.create_field('x_foo') field2 = self.create_field('x_bar') with self.assertRaises(IntegrityError), mute_logger('coffice.sql_db'): field2.name = field1.name
def test_proc_rule(self): # Create a product route containing a stock rule that will # generate a move from Stock for every procurement created in Output product_route = self.env['stock.location.route'].create({ 'name': 'Stock -> output route', 'product_selectable': True, 'rule_ids': [(0, 0, { 'name': 'Stock -> output rule', 'action': 'pull', 'picking_type_id': self.ref('stock.picking_type_internal'), 'location_src_id': self.ref('stock.stock_location_stock'), 'location_id': self.ref('stock.stock_location_output'), })], }) # Set this route on `product.product_product_3` self.product.write({'route_ids': [(4, product_route.id)]}) # Create Delivery Order of 10 `product.product_product_3` from Output -> Customer product = self.product vals = { 'name': 'Delivery order for procurement', 'partner_id': self.partner.id, 'picking_type_id': self.ref('stock.picking_type_out'), 'location_id': self.ref('stock.stock_location_output'), 'location_dest_id': self.ref('stock.stock_location_customers'), 'move_lines': [(0, 0, { 'name': '/', 'product_id': product.id, 'product_uom': product.uom_id.id, 'product_uom_qty': 10.00, 'procure_method': 'make_to_order', })], } pick_output = self.env['stock.picking'].create(vals) pick_output.move_lines.onchange_product_id() # Confirm delivery order. pick_output.action_confirm() # I run the scheduler. # Note: If purchase if already installed, the method _run_buy will be called due # to the purchase demo data. As we update the stock module to run this test, the # method won't be an attribute of stock.procurement at this moment. For that reason # we mute the logger when running the scheduler. with mute_logger('coffice.addons.stock.models.procurement'): self.env['procurement.group'].run_scheduler() # Check that a picking was created from stock to output. moves = self.env['stock.move'].search([ ('product_id', '=', self.product.id), ('location_id', '=', self.ref('stock.stock_location_stock')), ('location_dest_id', '=', self.ref('stock.stock_location_output')), ('move_dest_ids', 'in', [pick_output.move_lines[0].id]) ]) self.assertEqual( len(moves.ids), 1, "It should have created a picking from Stock to Output with the original picking as destination" )
def _update_foreign_keys(self, src_partners, dst_partner): """ Update all foreign key from the src_partner to dst_partner. All many2one fields will be updated. :param src_partners : merge source res.partner recordset (does not include destination one) :param dst_partner : record of destination res.partner """ _logger.debug( '_update_foreign_keys for dst_partner: %s for src_partners: %s', dst_partner.id, str(src_partners.ids)) # find the many2one relation to a partner Partner = self.env['res.partner'] relations = self._get_fk_on('res_partner') self.flush() for table, column in relations: if 'base_partner_merge_' in table: # ignore two tables continue # get list of columns of current table (exept the current fk column) query = "SELECT column_name FROM information_schema.columns WHERE table_name LIKE '%s'" % ( table) self._cr.execute(query, ()) columns = [] for data in self._cr.fetchall(): if data[0] != column: columns.append(data[0]) # do the update for the current table/column in SQL query_dic = { 'table': table, 'column': column, 'value': columns[0], } if len(columns) <= 1: # unique key treated query = """ UPDATE "%(table)s" as ___tu SET "%(column)s" = %%s WHERE "%(column)s" = %%s AND NOT EXISTS ( SELECT 1 FROM "%(table)s" as ___tw WHERE "%(column)s" = %%s AND ___tu.%(value)s = ___tw.%(value)s )""" % query_dic for partner in src_partners: self._cr.execute( query, (dst_partner.id, partner.id, dst_partner.id)) else: try: with mute_logger('coffice.sql_db'), self._cr.savepoint(): query = 'UPDATE "%(table)s" SET "%(column)s" = %%s WHERE "%(column)s" IN %%s' % query_dic self._cr.execute(query, ( dst_partner.id, tuple(src_partners.ids), )) # handle the recursivity with parent relation if column == Partner._parent_name and table == 'res_partner': query = """ WITH RECURSIVE cycle(id, parent_id) AS ( SELECT id, parent_id FROM res_partner UNION SELECT cycle.id, res_partner.parent_id FROM res_partner, cycle WHERE res_partner.id = cycle.parent_id AND cycle.id != cycle.parent_id ) SELECT id FROM cycle WHERE id = parent_id AND id = %s """ self._cr.execute(query, (dst_partner.id, )) # NOTE JEM : shouldn't we fetch the data ? except psycopg2.Error: # updating fails, most likely due to a violated unique constraint # keeping record with nonexistent partner_id is useless, better delete it query = 'DELETE FROM "%(table)s" WHERE "%(column)s" IN %%s' % query_dic self._cr.execute(query, (tuple(src_partners.ids), )) self.invalidate_cache()
def test_give_badge_without_badge(self): with mute_logger('coffice.sql_db'): with self.assertRaises(IntegrityError): self.certification_survey.write( {'certification_give_badge': True}) self.certification_survey.flush(['certification_give_badge'])