class ElectronicMail(ModelSQL, ModelView): "E-mail" __name__ = 'electronic_mail' mailbox = fields.Many2One('electronic_mail.mailbox', 'Mailbox', required=True) from_ = fields.Char('From') sender = fields.Char('Sender') to = fields.Char('To') cc = fields.Char('CC') bcc = fields.Char('BCC') subject = fields.Char('Subject') date = fields.DateTime('Date') message_id = fields.Char('Message-ID', help='Unique Message Identifier') in_reply_to = fields.Char('In-Reply-To') headers = fields.One2Many('electronic_mail.header', 'electronic_mail', 'Headers') digest = fields.Char('MD5 Digest', size=32) collision = fields.Integer('Collision') email = fields.Function(fields.Binary('Email'), 'get_email', 'set_email') flag_seen = fields.Boolean('Seen') flag_answered = fields.Boolean('Answered') flag_flagged = fields.Boolean('Flagged') flag_draft = fields.Boolean('Draft') flag_recent = fields.Boolean('Recent') size = fields.Integer('Size') mailbox_owner = fields.Function(fields.Many2One('res.user', 'Owner'), 'get_mailbox_owner', searcher='search_mailbox_owner') mailbox_read_users = fields.Function(fields.One2Many( 'res.user', None, 'Read Users'), 'get_mailbox_users', searcher='search_mailbox_users') mailbox_write_users = fields.Function(fields.One2Many( 'res.user', None, 'Write Users'), 'get_mailbox_users', searcher='search_mailbox_users') @staticmethod def default_collision(): return 0 @staticmethod def default_flag_seen(): return False @staticmethod def default_flag_answered(): return False @staticmethod def default_flag_flagged(): return False @staticmethod def default_flag_recent(): return False @classmethod def get_mailbox_owner(cls, mails, name): "Returns owner of mailbox" return dict([(mail.id, mail.mailbox.user.id) for mail in mails]) @classmethod def get_mailbox_users(cls, mails, name): assert name in ('mailbox_read_users', 'mailbox_write_users') res = {} for mail in mails: if name == 'mailbox_read_users': res[mail.id] = [x.id for x in mail.mailbox['read_users']] else: res[mail.id] = [x.id for x in mail.mailbox['write_users']] return res @classmethod def search_mailbox_owner(cls, name, clause): return [('mailbox.user', ) + clause[1:]] @classmethod def search_mailbox_users(cls, name, clause): return [('mailbox.' + name[8:], ) + clause[1:]] def _get_email(self): """ Returns the email object from reading the FS """ db_name = Transaction().cursor.dbname value = u'' if self.digest: filename = self.digest if self.collision: filename = filename + '-' + str(self.collision) filename = os.path.join(CONFIG['data_path'], db_name, 'email', filename[0:2], filename) try: with open(filename, 'r') as file_p: value = file_p.read() except IOError: pass return value @classmethod def get_email(cls, mails, name): """Fetches email from the data_path as email object """ result = {} for mail in mails: result[mail.id] = base64.encodestring(mail._get_email()) or False return result @classmethod def set_email(cls, mails, name, data): """Saves an email to the data path :param data: Email as string """ if data is False or data is None: return db_name = Transaction().cursor.dbname # Prepare Directory <DATA PATH>/<DB NAME>/email directory = os.path.join(CONFIG['data_path'], db_name) if not os.path.isdir(directory): os.makedirs(directory, 0770) digest = cls.make_digest(data) directory = os.path.join(directory, 'email', digest[0:2]) if not os.path.isdir(directory): os.makedirs(directory, 0770) # Filename <DIRECTORY>/<DIGEST> filename = os.path.join(directory, digest) collision = 0 if not os.path.isfile(filename): # File doesnt exist already with open(filename, 'w') as file_p: file_p.write(data) else: # File already exists, may be its the same email data # or maybe different. # Case 1: If different: we have to write file with updated # Collission index # Case 2: Same file: Leave it as such with open(filename, 'r') as file_p: data2 = file_p.read() if data != data2: cursor = Transaction().cursor cursor.execute( 'SELECT DISTINCT(collision) FROM electronic_mail ' 'WHERE digest = %s AND collision !=0 ' 'ORDER BY collision', (digest, )) collision2 = 0 for row in cursor.fetchall(): collision2 = row[0] filename = os.path.join(directory, digest + '-' + str(collision2)) if os.path.isfile(filename): with open(filename, 'r') as file_p: data2 = file_p.read() if data == data2: collision = collision2 break if collision == 0: collision = collision2 + 1 filename = os.path.join(directory, digest + '-' + str(collision)) with open(filename, 'w') as file_p: file_p.write(data) cls.write(mails, {'digest': digest, 'collision': collision}) @classmethod def make_digest(cls, data): """ Returns a digest from the mail :param data: Data String :return: Digest """ if hashlib: digest = hashlib.md5(data).hexdigest() else: digest = md5.new(data).hexdigest() return digest @classmethod def create_from_email(cls, mail, mailbox): """ Creates a mail record from a given mail :param mail: email object :param mailbox: ID of the mailbox """ Header = Pool().get('electronic_mail.header') email_date = mail.get('date') and datetime.fromtimestamp( mktime(parsedate(mail.get('date')))) values = { 'mailbox': mailbox, 'from_': mail.get('from'), 'sender': mail.get('sender'), 'to': mail.get('to'), 'cc': mail.get('cc'), 'bcc': mail.get('bcc'), 'subject': mail.get('subject'), 'date': email_date, 'message_id': mail.get('message-id'), 'in_reply_to': mail.get('in-reply-to'), 'email': mail.as_string(), 'size': getsizeof(mail.as_string()), } mail_created = cls.create([values])[0] Header.create_from_email(mail, mail_created.id) return mail_created
class Sample(metaclass=PoolMeta): __name__ = 'lims.sample' plant = fields.Function(fields.Many2One('lims.plant', 'Plant'), 'get_plant', searcher='search_plant') equipment = fields.Many2One('lims.equipment', 'Equipment', domain=['OR', ('id', '=', Eval('equipment')), ('party', '=', Eval('party'))], depends=['party'], select=True) equipment_template = fields.Function(fields.Many2One( 'lims.equipment.template', 'Equipment Template'), 'get_equipment_field') equipment_model = fields.Function(fields.Char('Equipment Model'), 'get_equipment_field') equipment_serial_number = fields.Function(fields.Char( 'Equipment Serial Number'), 'get_equipment_field') equipment_name = fields.Function(fields.Char( 'Equipment Name'), 'get_equipment_field') component = fields.Many2One('lims.component', 'Component', domain=['OR', ('id', '=', Eval('component')), ('equipment', '=', Eval('equipment'))], depends=['equipment']) comercial_product = fields.Many2One('lims.comercial.product', 'Comercial Product') ind_sampling_date = fields.Date('Sampling date') ind_volume = fields.Float('Received volume') sampling_type = fields.Many2One('lims.sampling.type', 'Sampling Type') ind_operational_detail = fields.Text('Operational detail') ind_work_environment = fields.Text('Work environment') ind_analysis_reason = fields.Text('Reason for analysis') missing_data = fields.Boolean('Missing data') attributes_domain = fields.Function(fields.Many2Many( 'lims.sample.attribute', None, None, 'Attributes domain'), 'on_change_with_attributes_domain') sample_photo = fields.Binary('Sample Photo', file_id='sample_photo_id', store_prefix='sample') sample_photo_id = fields.Char('Sample Photo ID', readonly=True) label_photo = fields.Binary('Label Photo', file_id='label_photo_id', store_prefix='sample') label_photo_id = fields.Char('Label Photo ID', readonly=True) oil_added = fields.Float('Liters Oil added') ind_equipment = fields.Integer('Equipment') ind_equipment_uom = fields.Selection([ ('hs', 'Hs.'), ('km', 'Km.'), ], 'UoM', sort=False) ind_component = fields.Integer('Component') ind_component_uom = fields.Selection([ ('hs', 'Hs.'), ('km', 'Km.'), ], 'UoM', sort=False) ind_oil = fields.Integer('Oil') ind_oil_uom = fields.Selection([ ('hs', 'Hs.'), ('km', 'Km.'), ], 'UoM', sort=False) oil_changed = fields.Selection([ (None, '-'), ('yes', 'Yes'), ('no', 'No'), ], 'Did change Oil?', sort=False) oil_changed_string = oil_changed.translated('oil_changed') oil_filter_changed = fields.Selection([ (None, '-'), ('yes', 'Yes'), ('no', 'No'), ], 'Did change Oil Filter?', sort=False) oil_filter_changed_string = oil_filter_changed.translated( 'oil_filter_changed') air_filter_changed = fields.Selection([ (None, '-'), ('yes', 'Yes'), ('no', 'No'), ], 'Did change Air Filter?', sort=False) air_filter_changed_string = air_filter_changed.translated( 'air_filter_changed') edition_log = fields.One2Many('lims.sample.edition.log', 'sample', 'Edition log', readonly=True) @classmethod def __register__(cls, module_name): cursor = Transaction().connection.cursor() table_h = cls.__table_handler__(module_name) sample = cls.__table__() super().__register__(module_name) if table_h.column_exist('changed_oil'): cursor.execute(*sample.update([sample.oil_changed], [Case((sample.changed_oil == Literal(True), 'yes'), else_='no')])) table_h.drop_column('changed_oil') if table_h.column_exist('changed_oil_filter'): cursor.execute(*sample.update([sample.oil_filter_changed], [Case((sample.changed_oil_filter == Literal(True), 'yes'), else_='no')])) table_h.drop_column('changed_oil_filter') if table_h.column_exist('changed_air_filter'): cursor.execute(*sample.update([sample.air_filter_changed], [Case((sample.changed_air_filter == Literal(True), 'yes'), else_='no')])) table_h.drop_column('changed_air_filter') if table_h.column_exist('hours_equipment'): cursor.execute(*sample.update([sample.ind_equipment], [sample.hours_equipment])) table_h.drop_column('hours_equipment') if table_h.column_exist('hours_component'): cursor.execute(*sample.update([sample.ind_component], [sample.hours_component])) table_h.drop_column('hours_component') if table_h.column_exist('hours_oil'): cursor.execute(*sample.update([sample.ind_oil], [sample.hours_oil])) table_h.drop_column('hours_oil') @classmethod def __setup__(cls): super().__setup__() cls.product_type.states['readonly'] = Bool(Eval('component')) if 'component' not in cls.product_type.depends: cls.product_type.depends.append('component') cls.matrix.states['readonly'] = Bool(Eval('comercial_product')) if 'comercial_product' not in cls.matrix.depends: cls.matrix.depends.append('comercial_product') cls.attributes.domain = [('id', 'in', Eval('attributes_domain'))] if 'attributes_domain' not in cls.attributes.depends: cls.attributes.depends.append('attributes_domain') @staticmethod def default_ind_equipment_uom(): return 'hs' @staticmethod def default_ind_component_uom(): return 'hs' @staticmethod def default_ind_oil_uom(): return 'hs' @fields.depends('equipment', 'component') def on_change_equipment(self): if not self.equipment and self.component: self.component = None @fields.depends('component') def on_change_component(self): if self.component: if self.component.product_type: self.product_type = self.component.product_type.id if self.component.comercial_product: self.comercial_product = self.component.comercial_product.id self.on_change_comercial_product() @fields.depends('comercial_product') def on_change_comercial_product(self): if self.comercial_product and self.comercial_product.matrix: self.matrix = self.comercial_product.matrix.id @fields.depends('product_type', '_parent_product_type.attribute_set') def on_change_with_attributes_domain(self, name=None): pool = Pool() SampleAttributeAttributeSet = pool.get( 'lims.sample.attribute-attribute.set') attribute_set = None if self.product_type and self.product_type.attribute_set: attribute_set = self.product_type.attribute_set.id res = SampleAttributeAttributeSet.search([ ('attribute_set', '=', attribute_set), ]) return [x.attribute.id for x in res] @classmethod def get_plant(cls, samples, name): result = {} for s in samples: result[s.id] = s.equipment and s.equipment.plant.id or None return result @classmethod def search_plant(cls, name, clause): return [('equipment.plant',) + tuple(clause[1:])] def _order_equipment_field(name): def order_field(tables): Equipment = Pool().get('lims.equipment') field = Equipment._fields[name] table, _ = tables[None] equipment_tables = tables.get('equipment') if equipment_tables is None: equipment = Equipment.__table__() equipment_tables = { None: (equipment, equipment.id == table.equipment), } tables['equipment'] = equipment_tables return field.convert_order(name, equipment_tables, Equipment) return staticmethod(order_field) order_plant = _order_equipment_field('plant') @classmethod def get_equipment_field(cls, samples, names): result = {} for name in names: result[name] = {} if cls._fields[name]._type == 'many2one': for s in samples: field = getattr(s.equipment, name.replace( 'equipment_', ''), None) result[name][s.id] = field.id if field else None else: for s in samples: result[name][s.id] = getattr(s.equipment, name.replace( 'equipment_', ''), None) return result @classmethod def order_component(cls, tables): Component = Pool().get('lims.component') kind_field = Component._fields['kind'] location_field = Component._fields['location'] sample, _ = tables[None] component_tables = tables.get('component') if component_tables is None: component = Component.__table__() component_tables = { None: (component, component.id == sample.component), } tables['component'] = component_tables order = ( kind_field.convert_order('kind', component_tables, Component) + location_field.convert_order('location', component_tables, Component)) return order @classmethod def _confirm_samples(cls, samples): TaskTemplate = Pool().get('lims.administrative.task.template') for sample in samples: if not sample.component or not sample.comercial_product: continue if sample.component.comercial_product != sample.comercial_product: sample.component.comercial_product = sample.comercial_product sample.component.save() TaskTemplate.create_tasks('sample_missing_data', cls._for_task_missing_data(samples)) TaskTemplate.create_tasks('sample_insufficient_volume', cls._for_task_required_volume(samples)) @classmethod def _for_task_missing_data(cls, samples): AdministrativeTask = Pool().get('lims.administrative.task') res = [] for sample in samples: if not sample.missing_data: continue if AdministrativeTask.search([ ('type', '=', 'sample_missing_data'), ('origin', '=', '%s,%s' % (cls.__name__, sample.id)), ('state', 'not in', ('done', 'discarded')), ]): continue res.append(sample) return res @classmethod def _for_task_required_volume(cls, samples): pool = Pool() EntryDetailAnalysis = pool.get('lims.entry.detail.analysis') AdministrativeTask = pool.get('lims.administrative.task') res = [] for sample in samples: received_volume = sample.ind_volume or 0 analysis_detail = EntryDetailAnalysis.search([ ('sample', '=', sample.id)]) for detail in analysis_detail: received_volume -= (detail.analysis.ind_volume or 0) if received_volume >= 0: continue if AdministrativeTask.search([ ('type', '=', 'sample_insufficient_volume'), ('origin', '=', '%s,%s' % (cls.__name__, sample.id)), ('state', 'not in', ('done', 'discarded')), ]): continue res.append(sample) return res @classmethod def delete(cls, samples): AdministrativeTask = Pool().get('lims.administrative.task') for sample in samples: tasks = AdministrativeTask.search([ ('origin', '=', '%s,%s' % (cls.__name__, sample.id)), ('state', 'not in', ('done', 'discarded')), ]) if tasks: AdministrativeTask.write(tasks, {'state': 'draft'}) AdministrativeTask.delete(tasks) super().delete(samples)
class Employee(metaclass=PoolMeta): __name__ = 'company.employee' photo = fields.Binary('Photo', file_id='photo_id') photo_id = fields.Char('Photo id', states={'invisible': True}) position = fields.Many2One('rrhh.position', 'Position', required=True, domain=[('department.company', '=', Eval('company'))], depends=['company']) department = fields.Function(fields.Char('Department'), 'get_department') contract_type = fields.Many2One('rrhh.contract.type', 'Contract type', required=True, domain=[('company', '=', Eval('company'))], depends=['company']) payment_type = fields.Many2One('rrhh.payment.type', 'Payment type', required=True) instruction_level = fields.Many2One('rrhh.instruction.level', 'Instruction level', required=True) documents = fields.One2Many('rrhh.document', 'employee', 'Documents') qualifications = fields.One2Many('rrhh.qualification', 'employee', 'Qualification') marital_status = fields.Function(fields.Char('Legal State'), 'get_marital_status') gender = fields.Function(fields.Char('Gender'), 'get_gender') birth_date = fields.Function(fields.Date('Birth date'), 'get_birth_date') birth_country = fields.Many2One('country.country', 'Country of birth', states={'required': True}) nationality = fields.Many2One('country.country', 'Nationality', states={'required': True}) residence = fields.Many2One('country.country', 'Country of residence', states={'required': True}) age = fields.Function(fields.Char('Age'), 'get_age') dependents = fields.One2Many('rrhh.dependent', 'employee', 'Dependents') @classmethod def __register__(cls, module_name): super(Employee, cls).__register__(module_name) table = cls.__table_handler__(module_name) # To 5.2 if table.column_exist('first_name'): table.drop_column('first_name') table.drop_column('middle_name') table.drop_column('last_name') # To 5.6.1 if table.column_exist('birth_date'): table.drop_column('birth_date') table.drop_column('genre') table.drop_column('marital_status') @classmethod def __setup__(cls): super(Employee, cls).__setup__() cls.company.states['readonly'] = True cls.party.domain = [ ('party_type', '=', 'person') ] @staticmethod def default_company(): return Transaction().context.get('company') @staticmethod def compute_age(party_dob): # TODO Translate with gettext if not party_dob: return 'No Birth date!' now = datetime.now() dob = datetime.strptime(str(party_dob), '%Y-%m-%d') delta = relativedelta(now, dob) years_months_days = str(delta.years) + 'y ' \ + str(delta.months) + 'm ' \ + str(delta.days) + 'd' return years_months_days def get_age(self, name): return self.compute_age(self.birth_date) def get_department(self, name): if self.position: return self.position.department.name def get_marital_status(self, name): return self._get_party_selection_string('person_legal_state') def get_gender(self, name): return self._get_party_selection_string('gender') def _get_party_selection_string(self, sel_name): pool = Pool() Trans = pool.get('ir.translation') Party = pool.get('party.party') if self.party: selection = getattr(Party, sel_name).selection sel = dict(selection)[getattr(self.party, sel_name)] lang = Transaction().context.get('language', None) if lang in (None, '', 'en'): return sel val = Trans.get_source( 'party.party,' + sel_name, 'selection', lang, sel) if not val: val = sel return val def get_birth_date(self, name): if self.party: return self.party.dob @fields.depends('position') def on_change_company(self): self.position = None
class NereidStaticFile(ModelSQL, ModelView): "Static files for Nereid" __name__ = "nereid.static.file" name = fields.Char('File Name', select=True, required=True) folder = fields.Many2One('nereid.static.folder', 'Folder', select=True, required=True) #: This function field returns the field contents. This is useful if the #: field is going to be displayed on the clients. file_binary = fields.Function( fields.Binary('File', filename='name'), 'get_file_binary', 'set_file_binary', ) #: Full path to the file in the filesystem file_path = fields.Function(fields.Char('File Path'), 'get_file_path') #: URL that can be used to idenfity the resource. Note that the value #: of this field is available only when called within a request context. #: In other words the URL is valid only when called in a nereid request. url = fields.Function(fields.Char('URL'), 'get_url') # Sequence sequence = fields.Integer('Sequence', select=True) # File mimetype mimetype = fields.Function(fields.Char('Mimetype'), getter='get_mimetype') @classmethod def __setup__(cls): super(NereidStaticFile, cls).__setup__() cls._sql_constraints += [ ('name_folder_uniq', 'UNIQUE(name, folder)', 'The Name of the Static File must be unique in a folder.!'), ] cls._error_messages.update({ 'invalid_file_name': """Invalid file name: (1) '..' in file name (OR) (2) file name contains '/'""", }) @staticmethod def default_sequence(): return 10 def get_mimetype(self, name): """ This method detects and returns the mimetype for the static file. The python mimetypes module returns a tuple of the form -: >>> mimetypes.guess_type(file_name) (file_mimetype, encoding) which can then be used to fill the `mimetype` field. Some example types are -: * image/png * application/pdf etc. """ return mimetypes.guess_type(self.name)[0] def get_url(self, name): """Return the url if within an active request context or return False values """ if _request_ctx_stack.top is None: return None return url_for('nereid.static.file.send_static_file', folder=self.folder.name, name=self.name) @staticmethod def get_nereid_base_path(): """ Returns base path for nereid, where all the static files would be stored. By Default it is: <Tryton Data Path>/<Database Name>/nereid """ cursor = Transaction().cursor return os.path.join(config.get('database', 'path'), cursor.database_name, "nereid") def _set_file_binary(self, value): """ Setter for static file that stores file in file system :param value: The value to set """ file_binary = fields.Binary.cast(bytes(value)) # If the folder does not exist, create it recursively directory = os.path.dirname(self.file_path) if not os.path.isdir(directory): os.makedirs(directory) with open(self.file_path, 'wb') as file_writer: file_writer.write(file_binary) @classmethod def set_file_binary(cls, files, name, value): """ Setter for the functional binary field. :param files: Records :param name: Ignored :param value: The file buffer """ for static_file in files: static_file._set_file_binary(value) def get_file_binary(self, name): ''' Getter for the binary_file field. This fetches the file from the file system, coverts it to buffer and returns it. :param name: Field name :return: Bytes ''' location = self.file_path with open(location, 'rb') as file_reader: return fields.Binary.cast(file_reader.read()) def get_file_path(self, name): """ Returns the full path to the file in the file system :param name: Field name :return: File path """ return os.path.abspath( os.path.join(self.get_nereid_base_path(), self.folder.name, self.name)) @classmethod def validate(cls, files): """ Validates the records. :param files: active record list of static files """ super(NereidStaticFile, cls).validate(files) for file in files: file.check_file_name() def check_file_name(self): ''' Check the validity of folder name Allowing the use of / or . will be risky as that could eventually lead to previlege escalation ''' if ('..' in self.name) or ('/' in self.name): self.raise_user_error("invalid_file_name") @classmethod @route("/static-file/<folder>/<name>", methods=["GET"]) def send_static_file(cls, folder, name): """ Invokes the send_file method in nereid.helpers to send a file as the response to the request. The file is sent in a way which is as efficient as possible. For example nereid will use the X-Send_file header to make nginx send the file if possible. :param folder: name of the folder :param name: name of the file """ # TODO: Separate this search and find into separate cached method files = cls.search([('folder.name', '=', folder), ('name', '=', name)]) if not files: abort(404) return send_file(files[0].file_path)
class TemplateImages(sequence_ordered('sequence', 'Orden de Listado'), ModelView, ModelSQL): 'Product Template Images' __name__ = 'product.template.images' template = fields.Many2One('product.template', 'Producto') image_name = fields.Function(fields.Char('File Name'), 'get_image_name') image_id = fields.Char('File ID', readonly=True, states={'invisible': True}) image = fields.Binary('Imagen', filename='image_name', file_id=file_id, store_prefix=None) image_f = fields.Function( fields.Binary('Imagen', filename='image_name', file_id=file_id, store_prefix=None), 'get_image_f') @classmethod def convert_photo(cls, data): if data and Image: image = Image.open(BytesIO(data)) # if image._getexif(): # exif = dict((ExifTags.TAGS[k], v) for k, v in image._getexif().items() if k in ExifTags.TAGS) # if exif.get('Orientation'): # if exif.get('Orientation')%2==0: # image = image.rotate(90, expand=True) image.thumbnail((388, 500), Image.ANTIALIAS) data = BytesIO() if not (image.format): image.save(data, 'JPEG') else: image.save(data, image.format) data = fields.Binary.cast(data.getvalue()) return data @classmethod def write(cls, *args): actions = iter(args) args = [] for product, vals in zip(actions, actions): if 'image' in vals: vals['image'] = cls.convert_photo(vals['image']) args.extend((product, vals)) super(TemplateImages, cls).write(*args) @classmethod def create(cls, vlist): vlist = [x.copy() for x in vlist] for values in vlist: if 'image' in values: values['image'] = cls.convert_photo(values['image']) return super(TemplateImages, cls).create(vlist) def get_img(self): foto = None if self.image_id: return '/static/img2/' + self.image_id[:2] + '/' + self.image_id[ 2:4] + '/' + self.image_id if self.image: import base64 foto = base64.b64encode(self.image).decode() return "data:image/JPEG;base64," + foto return None def get_image_f(self, name): if self.image: return self.image return None def get_image_name(self, name): file_name = '' if self.template: if self.template.rec_name: file_name = self.template.rec_name + ".jpg" return file_name def get_rec_name(self, name): if self.template: return self.template.name
class Attachment(ResourceMixin, ModelSQL, ModelView): "Attachment" __name__ = 'ir.attachment' name = fields.Char('Name', required=True) type = fields.Selection([ ('data', 'Data'), ('link', 'Link'), ], 'Type', required=True) description = fields.Text('Description') summary = fields.Function(fields.Char('Summary'), 'on_change_with_summary') link = fields.Char('Link', states={ 'invisible': Eval('type') != 'link', }, depends=['type']) data = fields.Binary('Data', filename='name', file_id=file_id, store_prefix=store_prefix, states={ 'invisible': Eval('type') != 'data', }, depends=['type']) file_id = fields.Char('File ID', readonly=True) data_size = fields.Function( fields.Integer('Data size', states={ 'invisible': Eval('type') != 'data', }, depends=['type']), 'get_size') @classmethod def __setup__(cls): super().__setup__() cls._order = [ ('create_date', 'DESC'), ('id', 'DESC'), ] @classmethod def __register__(cls, module_name): cursor = Transaction().connection.cursor() super(Attachment, cls).__register__(module_name) table = cls.__table_handler__(module_name) attachment = cls.__table__() # Migration from 4.0: merge digest and collision into file_id if table.column_exist('digest') and table.column_exist('collision'): cursor.execute( *attachment.update([attachment.file_id], [attachment.digest], where=(attachment.collision == 0) | (attachment.collision == Null))) cursor.execute(*attachment.update( [attachment.file_id], [Concat(Concat(attachment.digest, '-'), attachment.collision)], where=(attachment.collision != 0) & (attachment.collision != Null))) table.drop_column('digest') table.drop_column('collision') # Migration from 4.8: remove unique constraint table.drop_constraint('resource_name') @staticmethod def default_type(): return 'data' def get_size(self, name): with Transaction().set_context({ '%s.%s' % (self.__name__, name): 'size', }): record = self.__class__(self.id) return record.data @fields.depends('description') def on_change_with_summary(self, name=None): return firstline(self.description or '') @classmethod def fields_view_get(cls, view_id=None, view_type='form', level=None): pool = Pool() ModelData = pool.get('ir.model.data') if not view_id: if Transaction().context.get('preview'): view_id = ModelData.get_id('ir', 'attachment_view_form_preview') return super().fields_view_get(view_id=view_id, view_type=view_type, level=level)
class ReportTemplateSection(ModelSQL, ModelView): 'Report Template Section' __name__ = 'lims.report.template.section' _order_name = 'order' template = fields.Many2One('lims.report.template', 'Template', ondelete='CASCADE', select=True, required=True) name = fields.Char('Name', required=True) data = fields.Binary('File', filename='name', required=True, file_id='data_id', store_prefix='results_report_template_section') data_id = fields.Char('File ID', readonly=True) position = fields.Selection([ ('previous', 'Previous'), ('following', 'Following'), ], 'Position', required=True) order = fields.Integer('Order') @classmethod def __setup__(cls): super().__setup__() cls._order.insert(0, ('order', 'ASC')) @classmethod def __register__(cls, module_name): cursor = Transaction().connection.cursor() TableHandler = backend.TableHandler old_table_exist = TableHandler.table_exist( 'lims_result_report_template_section') if old_table_exist: cursor.execute('ALTER TABLE ' 'lims_result_report_template_section ' 'RENAME TO lims_report_template_section ') cursor.execute('ALTER INDEX ' 'lims_result_report_template_section_pkey ' 'RENAME TO lims_report_template_section_pkey') cursor.execute( 'ALTER INDEX ' 'lims_result_report_template_section_template_index ' 'RENAME TO lims_report_template_section_template_index') cursor.execute('ALTER SEQUENCE ' 'lims_result_report_template_section_id_seq ' 'RENAME TO lims_report_template_section_id_seq') super().__register__(module_name) @classmethod def validate(cls, sections): super().validate(sections) merger = PdfFileMerger(strict=False) for section in sections: filedata = BytesIO(section.data) try: merger.append(filedata) except PdfReadError: raise UserError(gettext('lims_report_html.msg_section_pdf'))
class People(ModelSQL, ModelView): "People" _name = "auroville.people" _description = __doc__ #TODO uniq id asynctoid = fields.Char('Asyncto Id', required=False, select=False, readonly=True) aurovillename = fields.Char('Auroville name', required=True, translate=False, select=True) name = fields.Char('Given name', translate=False, select=True) surname = fields.Char('Surname', translate=False, select=True) telephone = fields.Char('Phone number', translate=False) email = fields.Char('Email', translate=False, select=True) contactperson = fields.Char('Contact person', translate=False, select=True) masterlistid = fields.Integer('Masterlist ID', readonly=True, select=True) date_birth = fields.Date('Date of birth') sex = fields.Selection([ ('Male', 'Male'), ('Female', 'Female'), ], 'Sex', required=True) photo = fields.Binary('Picture') nationality = fields.Many2One('auroville.nationality', 'Nationality', select=True) age = fields.Function(fields.Integer('Age'), 'get_people_age') status = fields.Selection([ ('Aurovilian', 'Aurovilian'), ('Newcomer', 'Newcomer'), ('Pre-Newcomer', 'Pre-Newcomer'), ('Volunteer', 'Volunteer'), ('Youth', 'Youth'), ('Child', 'Child'), ('Friend of Auroville', 'Friend of Auroville'), ('Guest', 'Guest'), ], 'Status', required=True) user_id = fields.Many2One('res.user', 'User', required=True, readonly=True) def __init__(self): super(People, self).__init__() self._order.insert(0, ('masterlistid', 'ASC')) def get_people_age(self, ids, name): today = date.today() res = {} for human in self.browse(ids): born = human.date_birth if born: res[human.id] = today.year - born.year - int( (today.month, today.day) < (born.month, born.day)) else: res[human.id] = 0 return res def default_user_id(self): user_obj = Pool().get('res.user') user = user_obj.browse(Transaction().user) return int(user.id) def default_sex(self): return 'Male' def default_status(self): return 'Aurovilian' def asyncto_sync(self): log = logging.getLogger('logfile') log.warn('Asyncto People sync') try: asyncto_obj = Pool().get('auroville.asyncto') people_obj = Pool().get('auroville.people') asyncto_ids = asyncto_obj.search([]) for item in asyncto_obj.browse(asyncto_ids): people_ids = people_obj.search([ ('asynctoid', '=', item.asynctoid), ]) data = { 'asynctoid': item.asynctoid, 'aurovillename': item.aurovillename, 'name': item.name, 'surname': item.surname, 'telephone': item.telephone, 'email': item.email, 'contactperson': item.contactperson, 'masterlistid': int(item.masterlistid) } if people_ids: people_obj.write(people_ids, data) else: people_obj.create(data) # Community except Exception as e: log.error(e)
class Binary(ModelSQL): 'Binary' __name__ = 'test.binary' binary = fields.Binary('Binary')
class NereidStaticFile(ModelSQL, ModelView): "Static files for Nereid" __name__ = "nereid.static.file" name = fields.Char('File Name', select=True, required=True) folder = fields.Many2One('nereid.static.folder', 'Folder', select=True, required=True) type = fields.Selection([ ('local', 'Local File'), ('remote', 'Remote File'), ], 'File Type') #: URL of the remote file if the :attr:`type` is remote remote_path = fields.Char('Remote File', select=True, translate=True, states={ 'required': Equal(Eval('type'), 'remote'), 'invisible': Not(Equal(Eval('type'), 'remote')) }) #: This function field returns the field contents. This is useful if the #: field is going to be displayed on the clients. file_binary = fields.Function( fields.Binary('File', filename='name'), 'get_file_binary', 'set_file_binary', ) #: Full path to the file in the filesystem file_path = fields.Function(fields.Char('File Path'), 'get_file_path') #: URL that can be used to idenfity the resource. Note that the value #: of this field is available only when called within a request context. #: In other words the URL is valid only when called in a nereid request. url = fields.Function(fields.Char('URL'), 'get_url') @classmethod def __setup__(cls): super(NereidStaticFile, cls).__setup__() cls._constraints += [ ('check_file_name', 'invalid_file_name'), ] cls._sql_constraints += [ ('name_folder_uniq', 'UNIQUE(name, folder)', 'The Name of the Static File must be unique in a folder.!'), ] cls._error_messages.update({ 'invalid_file_name': """Invalid file name: (1) '..' in file name (OR) (2) file name contains '/'""", }) @staticmethod def default_type(): return 'local' def get_url(self, name): """Return the url if within an active request context or return False values """ if _request_ctx_stack.top is None: return None if self.type == 'local': return url_for('nereid.static.file.send_static_file', folder=self.folder.folder_name, name=self.name) elif self.type == 'remote': return self.remote_path @staticmethod def get_nereid_base_path(): """ Returns base path for nereid, where all the static files would be stored. By Default it is: <Tryton Data Path>/<Database Name>/nereid """ cursor = Transaction().cursor return os.path.join(CONFIG['data_path'], cursor.database_name, "nereid") def _set_file_binary(self, value): """ Setter for static file that stores file in file system :param value: The value to set """ if self.type == 'local': file_binary = buffer(value) # If the folder does not exist, create it recursively directory = os.path.dirname(self.file_path) if not os.path.isdir(directory): os.makedirs(directory) with open(self.file_path, 'wb') as file_writer: file_writer.write(file_binary) @classmethod def set_file_binary(cls, files, name, value): """ Setter for the functional binary field. :param files: Records :param name: Ignored :param value: The file buffer """ for static_file in files: static_file._set_file_binary(value) def get_file_binary(self, name): ''' Getter for the binary_file field. This fetches the file from the file system, coverts it to buffer and returns it. :param name: Field name :return: File buffer ''' location = self.file_path if self.type == 'local' \ else urllib.urlretrieve(self.remote_path)[0] with open(location, 'rb') as file_reader: return buffer(file_reader.read()) def get_file_path(self, name): """ Returns the full path to the file in the file system :param name: Field name :return: File path """ return os.path.abspath( os.path.join( self.get_nereid_base_path(), self.folder.folder_name, self.name )) \ if self.type == 'local' else self.remote_path def check_file_name(self): ''' Check the validity of folder name Allowing the use of / or . will be risky as that could eventually lead to previlege escalation ''' if ('..' in self.name) or ('/' in self.name): return False return True @classmethod def send_static_file(cls, folder, name): """ Invokes the send_file method in nereid.helpers to send a file as the response to the request. The file is sent in a way which is as efficient as possible. For example nereid will use the X-Send_file header to make nginx send the file if possible. :param folder: folder_name of the folder :param name: name of the file """ #TODO: Separate this search and find into separate cached method files = cls.search([('folder.folder_name', '=', folder), ('name', '=', name)]) if not files: abort(404) return send_file(files[0].file_path)
class Message(Workflow, ModelSQL, ModelView): 'SEPA Message' __name__ = 'account.payment.sepa.message' _states = { 'readonly': Eval('state') != 'draft', } _depends = ['state'] message = fields.Binary('Message', filename='filename', file_id=file_id, store_prefix=store_prefix, states=_states, depends=_depends) message_file_id = fields.Char("Message File ID", readonly=True) filename = fields.Function(fields.Char('Filename'), 'get_filename') type = fields.Selection([ ('in', 'IN'), ('out', 'OUT'), ], 'Type', required=True, states=_states, depends=_depends) company = fields.Many2One( 'company.company', 'Company', required=True, select=True, domain=[ ('id', If(Eval('context', {}).contains('company'), '=', '!='), Eval('context', {}).get('company', -1)), ], states={ 'readonly': Eval('state') != 'draft', }, depends=['state']) origin = fields.Reference('Origin', selection='get_origin', select=True, states=_states, depends=_depends) state = fields.Selection([ ('draft', 'Draft'), ('waiting', 'Waiting'), ('done', 'Done'), ('canceled', 'Canceled'), ], 'State', readonly=True, select=True) @classmethod def __setup__(cls): super(Message, cls).__setup__() cls._transitions |= { ('draft', 'waiting'), ('waiting', 'done'), ('waiting', 'draft'), ('draft', 'canceled'), ('waiting', 'canceled'), } cls._buttons.update({ 'cancel': { 'invisible': ~Eval('state').in_(['draft', 'waiting']), 'depends': ['state'], }, 'draft': { 'invisible': Eval('state') != 'waiting', 'depends': ['state'], }, 'wait': { 'invisible': Eval('state') != 'draft', 'depends': ['state'], }, 'do': { 'invisible': Eval('state') != 'waiting', 'depends': ['state'], }, }) @classmethod def __register__(cls, module_name): TableHandler = backend.get('TableHandler') cursor = Transaction().connection.cursor() pool = Pool() Group = pool.get('account.payment.group') super(Message, cls).__register__(module_name) # Migration from 3.2 if TableHandler.table_exist(Group._table): group_table = Group.__table_handler__(module_name) if group_table.column_exist('sepa_message'): group = Group.__table__() table = cls.__table__() cursor.execute( *group.select(group.id, group.sepa_message, group.company)) for group_id, message, company_id in cursor.fetchall(): cursor.execute(*table.insert([ table.message, table.type, table.company, table.origin, table.state ], [[ message, 'out', company_id, 'account.payment.group,%s' % group_id, 'done' ]])) group_table.drop_column('sepa_message') @staticmethod def default_type(): return 'in' @staticmethod def default_company(): return Transaction().context.get('company') @staticmethod def default_state(): return 'draft' def get_filename(self, name): pool = Pool() Group = pool.get('account.payment.group') if isinstance(self.origin, Group): return self.origin.rec_name + '.xml' @staticmethod def _get_origin(): 'Return list of Model names for origin Reference' return ['account.payment.group'] @classmethod def get_origin(cls): IrModel = Pool().get('ir.model') models = cls._get_origin() models = IrModel.search([ ('model', 'in', models), ]) return [(None, '')] + [(m.model, m.name) for m in models] @classmethod @ModelView.button @Workflow.transition('draft') def draft(cls, messages): pass @classmethod @ModelView.button @Workflow.transition('waiting') def wait(cls, messages): pass @classmethod @ModelView.button @Workflow.transition('done') def do(cls, messages): for message in messages: if message.type == 'in': message.parse() else: message.send() @classmethod @ModelView.button @Workflow.transition('canceled') def cancel(cls, messages): pass @staticmethod def _get_handlers(): pool = Pool() Payment = pool.get('account.payment') return { 'urn:iso:std:iso:20022:tech:xsd:camt.054.001.01': lambda f: CAMT054(f, Payment), 'urn:iso:std:iso:20022:tech:xsd:camt.054.001.02': lambda f: CAMT054(f, Payment), 'urn:iso:std:iso:20022:tech:xsd:camt.054.001.03': lambda f: CAMT054(f, Payment), 'urn:iso:std:iso:20022:tech:xsd:camt.054.001.04': lambda f: CAMT054(f, Payment), } @staticmethod def get_namespace(message): f = BytesIO(message) for _, element in etree.iterparse(f, events=('start', )): tag = etree.QName(element) if tag.localname == 'Document': return tag.namespace def parse(self): f = BytesIO(self.message) namespace = self.get_namespace(self.message) handlers = self._get_handlers() if namespace not in handlers: raise # TODO UserError handlers[namespace](f) def send(self): pass
class Party(ModelSQL, ModelView): "Party" __name__ = 'party.party' #avatar avatar = fields.Function(fields.Binary('Avatar', filename='avatar_filename', help='Avatar User Image'), 'get_avatar', setter='set_avatar') avatar_filename = fields.Char('Avatar File Name', help='Avatar File Name') name = fields.Char('Name', required=True, select=True, states=STATES, depends=DEPENDS) code = fields.Char('Code', required=True, select=True, states={ 'readonly': Eval('code_readonly', True), }, depends=['code_readonly']) code_readonly = fields.Function(fields.Boolean('Code Readonly'), 'get_code_readonly') lang = fields.Many2One("ir.lang", 'Language', states=STATES, depends=DEPENDS) vat_number = fields.Char('VAT Number', help="Value Added Tax number", states={ 'readonly': ~Eval('active', True), 'required': Bool(Eval('vat_country')), }, depends=['active', 'vat_country']) vat_country = fields.Selection( VAT_COUNTRIES, 'VAT Country', states=STATES, depends=DEPENDS, help="Setting VAT country will enable validation of the VAT number.", translate=False) vat_code = fields.Function(fields.Char('VAT Code'), 'on_change_with_vat_code', searcher='search_vat_code') addresses = fields.One2Many('party.address', 'party', 'Addresses', states=STATES, depends=DEPENDS) contact_mechanisms = fields.One2Many('party.contact_mechanism', 'party', 'Contact Mechanisms', states=STATES, depends=DEPENDS) categories = fields.Many2Many('party.party-party.category', 'party', 'category', 'Categories', states=STATES, depends=DEPENDS) active = fields.Boolean('Active', select=True) full_name = fields.Function(fields.Char('Full Name'), 'get_full_name') phone = fields.Function(fields.Char('Phone'), 'get_mechanism') mobile = fields.Function(fields.Char('Mobile'), 'get_mechanism') fax = fields.Function(fields.Char('Fax'), 'get_mechanism') email = fields.Function(fields.Char('E-Mail'), 'get_mechanism') website = fields.Function(fields.Char('Website'), 'get_mechanism') # photo = fields.Binary('Picture') # description = fields.Text("Description", translate=True, states=STATES, # depends=DEPENDS) @classmethod def __setup__(cls): super(Party, cls).__setup__() cls._sql_constraints = [('code_uniq', 'UNIQUE(code)', 'The code of the party must be unique.')] cls._error_messages.update({ 'invalid_vat': ('Invalid VAT number "%(vat)s" on party ' '"%(party)s".'), }) cls._order.insert(0, ('name', 'ASC')) def get_avatar(self, name): db_name = Transaction().cursor.dbname filename = self.avatar_filename if not filename: return None filename = os.path.join(config.get('database', 'path'), db_name, 'user', 'avatar', filename[0:2], filename[2:4], filename) value = None try: with open(filename, 'rb') as file_p: value = buffer(file_p.read()) except IOError: pass return value @classmethod def set_avatar(cls, users, name, value): if value is None: return if not value: cls.write(users, { 'avatar_filename': None, }) return db_name = Transaction().cursor.dbname user_dir = os.path.join(config.get('database', 'path'), db_name, 'user', 'avatar') if not value: cls.write(users, { 'avatar_filename': None, }) return for user in users: file_name = user['avatar_filename'] file_mime, _ = guess_type(file_name) if not file_mime: cls.raise_user_error('not_file_mime', { 'file_name': file_name, }) if file_mime not in IMAGE_TYPES: cls.raise_user_error('not_file_mime_image', { 'file_name': file_name, }) _, ext = file_mime.split('/') digest = '%s.%s' % (hashlib.md5(value).hexdigest(), ext) subdir1 = digest[0:2] subdir2 = digest[2:4] directory = os.path.join(user_dir, subdir1, subdir2) filename = os.path.join(directory, digest) if not os.path.isdir(directory): os.makedirs(directory, 0775) os.umask(0022) with open(filename, 'wb') as file_p: file_p.write(value) # square and thumbnail thumb image thumb_size = AVATAR_SIZE, AVATAR_SIZE try: im = Image.open(filename) except: if os.path.exists(filename): os.remove(filename) cls.raise_user_error('not_file_mime_image', { 'file_name': file_name, }) width, height = im.size if width > height: delta = width - height left = int(delta / 2) upper = 0 right = height + left lower = height else: delta = height - width left = 0 upper = int(delta / 2) right = width lower = width + upper im = im.crop((left, upper, right, lower)) im.thumbnail(thumb_size, Image.ANTIALIAS) im.save(filename) cls.write([user], { 'avatar_filename': digest, }) @staticmethod def order_code(tables): table, _ = tables[None] return [CharLength(table.code), table.code] @staticmethod def default_active(): return True @staticmethod def default_categories(): return Transaction().context.get('categories', []) @staticmethod def default_addresses(): if Transaction().user == 0: return [] Address = Pool().get('party.address') fields_names = list(x for x in Address._fields.keys() if x not in ('id', 'create_uid', 'create_date', 'write_uid', 'write_date')) return [Address.default_get(fields_names)] @staticmethod def default_lang(): Configuration = Pool().get('party.configuration') config = Configuration(1) if config.party_lang: return config.party_lang.id @staticmethod def default_code_readonly(): Configuration = Pool().get('party.configuration') config = Configuration(1) return bool(config.party_sequence) def get_code_readonly(self, name): return True @fields.depends('vat_country', 'vat_number') def on_change_with_vat_code(self, name=None): return (self.vat_country or '') + (self.vat_number or '') @classmethod def search_vat_code(cls, name, clause): res = [] value = clause[2] for country, _ in VAT_COUNTRIES: if isinstance(value, basestring) \ and country \ and value.upper().startswith(country): res.append(('vat_country', '=', country)) value = value[len(country):] break res.append(('vat_number', clause[1], value)) return res def get_full_name(self, name): return self.name def get_mechanism(self, name): for mechanism in self.contact_mechanisms: if mechanism.type == name: return mechanism.value return '' @classmethod def create(cls, vlist): Sequence = Pool().get('ir.sequence') Configuration = Pool().get('party.configuration') vlist = [x.copy() for x in vlist] for values in vlist: if not values.get('code'): config = Configuration(1) values['code'] = Sequence.get_id(config.party_sequence.id) values.setdefault('addresses', None) return super(Party, cls).create(vlist) @classmethod def copy(cls, parties, default=None): if default is None: default = {} default = default.copy() default['code'] = None return super(Party, cls).copy(parties, default=default) @classmethod def search_global(cls, text): for id_, rec_name, icon in super(Party, cls).search_global(text): icon = icon or 'tryton-party' yield id_, rec_name, icon @classmethod def search_rec_name(cls, name, clause): return [ 'OR', ('code', ) + tuple(clause[1:]), ('name', ) + tuple(clause[1:]), ] def address_get(self, type=None): """ Try to find an address for the given type, if no type matches the first address is returned. """ Address = Pool().get("party.address") addresses = Address.search([("party", "=", self.id), ("active", "=", True)], order=[('sequence', 'ASC'), ('id', 'ASC')]) if not addresses: return None default_address = addresses[0] if not type: return default_address for address in addresses: if getattr(address, type): return address return default_address @classmethod def validate(cls, parties): super(Party, cls).validate(parties) for party in parties: party.check_vat() def check_vat(self): ''' Check the VAT number depending of the country. http://sima-pc.com/nif.php ''' if not HAS_VATNUMBER: return vat_number = self.vat_number if not self.vat_country: return if not getattr(vatnumber, 'check_vat_' + self.vat_country.lower())(vat_number): #Check if user doesn't have put country code in number if vat_number.startswith(self.vat_country): vat_number = vat_number[len(self.vat_country):] Party.write([self], { 'vat_number': vat_number, }) else: self.raise_user_error('invalid_vat', { 'vat': vat_number, 'party': self.rec_name, })
class Service(Workflow, ModelSQL, ModelView): 'Service' __name__ = 'service.service' __history = True company = fields.Many2One( 'company.company', 'Company', required=True, readonly=True, select=True, domain=[ ('id', If(Eval('context', {}).contains('company'), '=', '!='), Eval('context', {}).get('company', -1)), ], depends=_DEPENDS) party = fields.Many2One('party.party', 'Party', states=_STATES, required=True) number_service = fields.Char('No. Comprobante', readonly=True) type = fields.Selection(_TYPE, 'Type', select=True, states={ 'readonly': ((Eval('state') == 'delivered') | Eval('context', {}).get('type')), }) total = fields.Function( fields.Numeric('Total', states={ 'invisible': Eval('type') == 'home_service', }), 'get_amount') entry_date = fields.Date('Entry Date', states=_STATES, domain=[('entry_date', '<', Eval('delivery_date', None))], depends=['delivery_date']) delivery_date = fields.Date('Estimated Delivery Date', states=_STATES, domain=[('delivery_date', '>', Eval('entry_date', None))], depends=['entry_date']) technical = fields.Many2One('company.employee', 'Technical', states=_STATES) garanty = fields.Boolean('Garanty', help="Income Garanty", states=_STATES) new = fields.Boolean('New', states={ 'invisible': ~Eval('garanty', True), 'readonly': Eval('state') == 'delivered', }) lined = fields.Boolean('Lined', states={ 'invisible': ~Eval('garanty', True), 'readonly': Eval('state') == 'delivered', }) beaten = fields.Boolean('Beaten', states={ 'invisible': ~Eval('garanty', True), 'readonly': Eval('state') == 'delivered', }) broken = fields.Boolean('Broken', states={ 'invisible': ~Eval('garanty', True), 'readonly': Eval('state') == 'delivered', }) stained = fields.Boolean('Stained', states={ 'invisible': ~Eval('garanty', True), 'readonly': Eval('state') == 'delivered', }) invoice_date = fields.Date('Invoice Date', states={ 'invisible': ~Eval('garanty', True), 'readonly': Eval('state') == 'delivered', }) invoice_number = fields.Char('Invoice number', states={ 'invisible': ~Eval('garanty', True), 'readonly': Eval('state') == 'delivered', }) case_number = fields.Char('Case number', states={ 'invisible': ~Eval('garanty', True), 'readonly': Eval('state') == 'delivered', }) send_date = fields.Date('Send Date', states={ 'invisible': ~Eval('garanty', True), 'readonly': Eval('state') == 'delivered', }) remission = fields.Char('No. guide remission', states={ 'invisible': ~Eval('garanty', True), 'readonly': Eval('state') == 'delivered', }) transport = fields.Char('Transport', states={ 'invisible': ~Eval('garanty', True), 'readonly': Eval('state') == 'delivered', }) photo = fields.Binary('Foto', states=_STATES) state = fields.Selection([('pending', 'Pending'), ('review', 'In Review'), ('ready', 'Ready'), ('without', 'Without Solution'), ('warranty', 'Warranty not cover'), ('delivered', 'Delivered')], 'State', readonly=True) lines = fields.One2Many('service.service.line', 'service', 'Lines', states=_STATES) accessories = fields.Text('Accessories', states={ 'readonly': Eval('accessories') != '', }) observations = fields.Text('Observations', states=_STATES) history_lines = fields.One2Many('service.service.history_lines', 'service', 'Lines') total_home_service = fields.Numeric('Total', states={ 'invisible': Eval('type') == 'service', }) detail = fields.Text('Repair Detail', states={ 'invisible': Eval('state') != 'delivered', 'readonly': Eval('detail') != '', }) state_date = fields.Function(fields.Char('State Date'), 'get_state_date') equipo = fields.Char('Equipo') marca = fields.Char('Marca') modelo = fields.Char('Modelo') direccion = fields.Char(u'Dirección', states={ 'invisible': ~Eval('party'), }) telefono = fields.Char(u'Teléfono', states={ 'invisible': ~Eval('party'), }) correo = fields.Char(u'Correo Electrónico', states={ 'invisible': ~Eval('party'), }) @classmethod def __setup__(cls): super(Service, cls).__setup__() cls.__rpc__['getTechnicalService'] = RPC(check_access=False, readonly=False) cls._error_messages.update({ 'modify_invoice': ('You can not modify service "%s".'), 'delete_cancel': ('You can not delete service "%s".'), }) cls._transitions |= set(( ('pending', 'review'), ('review', 'ready'), ('review', 'without'), ('review', 'warranty'), ('ready', 'delivered'), ('without', 'delivered'), ('warranty', 'delivered'), )) cls._buttons.update({ 'review': { 'invisible': Eval('state') != ('pending') }, 'ready': { 'invisible': Eval('state').in_(['pending', 'ready', 'without', 'delivered']) }, 'without': { 'invisible': Eval('state').in_(['pending', 'ready', 'without', 'delivered']) }, 'warranty': { 'invisible': ~Eval('garanty', True) | (Eval('state').in_( ['pending', 'ready', 'without', 'delivered'])) }, 'delivered': { 'invisible': Eval('state').in_(['review', 'pending', 'delivered']) }, }) @fields.depends('invoice_date', 'garanty') def on_change_invoice_date(self): res = {} Date = Pool().get('ir.date') if self.garanty != None: year = Date.today() - datetime.timedelta(days=365) if self.invoice_date < year: res['invoice_date'] = self.invoice_date self.raise_user_error( u'Está seguro de la fecha de ingreso: "%s"' u'tiene mas de un año de garantia', (self.invoice_date)) else: res['invoice_date'] = self.invoice_date return res @fields.depends('party') def on_change_party(self): res = {} if self.party: if self.party.addresses[0]: res['direccion'] = self.party.addresses[0].street else: res['direccion'] = "" if self.party.phone: res['telefono'] = self.party.phone elif self.party.mobile: res['telefono'] = self.party.mobile else: res['telefono'] = "" if self.party.email: res['correo'] = self.party.email else: res['correo'] = "" else: res['direccion'] = "" res['telefono'] = "" res['correo'] = "" return res @fields.depends('lines') def on_change_lines(self): res = {} if self.lines: for line in self.lines: if line.product: res['equipo'] = line.periferic.name else: res['equipo'] = "" if line.trademark: res['marca'] = line.trademark.name else: res['marca'] = "" if line.model: res['modelo'] = line.model else: res['modelo'] = "" return res @classmethod def get_state_date(cls, services, names): pool = Pool() Date = pool.get('ir.date') date_now = Date.today() result = {n: {s.id: '' for s in services} for n in names} for name in names: for service in services: if (service.delivery_date < date_now) and (service.state != 'delivered'): result[name][service.id] = 'vencida' elif (service.delivery_date == date_now) and (service.state != 'delivered'): result[name][service.id] = 'vence_hoy' else: result[name][service.id] = '' return result @staticmethod def default_entry_date(): Date = Pool().get('ir.date') return Date.today() @staticmethod def default_accessories(): return '' @staticmethod def default_detail(): return '' @staticmethod def default_delivery_date(): Date = Pool().get('ir.date') return Date.today() + datetime.timedelta(days=1) @staticmethod def default_state(): return 'pending' @staticmethod def default_type(): return Transaction().context.get('type', 'service') @staticmethod def default_company(): return Transaction().context.get('company') @classmethod def get_amount(cls, services, names): amount = Decimal(0.0) total = dict((i.id, _ZERO) for i in services) for service in services: for line in service.lines: if line.reference_amount: amount += line.reference_amount total[service.id] = amount result = { 'total': total, } for key in result.keys(): if key not in names: del result[key] return result @fields.depends('total', 'total_home_service') def on_change_total_home_service(self): res = {} if self.total_home_service: res['total'] = self.total_home_service else: res['total'] = Decimal(0.0) return res def set_number(self): pool = Pool() Period = pool.get('account.period') Sequence = pool.get('ir.sequence.strict') Date = pool.get('ir.date') if self.number_service: return test_state = True accounting_date = self.entry_date period_id = Period.find(self.company.id, date=accounting_date, test_state=test_state) period = Period(period_id) sequence = period.get_service_sequence(self.type) if not sequence: self.raise_user_error('no_withholding_sequence', { 'withholding': self.rec_name, 'period': period.rec_name, }) with Transaction().set_context(date=self.entry_date or Date.today()): number = Sequence.get_id(sequence.id) vals = {'number_service': number} if (not self.entry_date and self.type in ('service')): vals['entry_date'] = Transaction().context['date'] self.write([self], vals) @classmethod def check_modify(cls, services): for service in services: if (service.state in ('delivered')): cls.raise_user_error('modify_invoice', (service.number_service, )) @classmethod def delete(cls, services): cls.check_modify(services) for service in services: if (service.state in ('review', 'ready', 'without', 'warranty', 'delivered')): cls.raise_user_error('delete_cancel', (service.number_service, )) super(Service, cls).delete(services) @classmethod def copy(cls, services, default=None): if default is None: default = {} default = default.copy() default['state'] = 'pending' default['number_service'] = None default['type'] = 'service' return super(Service, cls).copy(services, default=default) @classmethod @ModelView.button @Workflow.transition('review') def review(cls, services): for service in services: pool = Pool() Contact = pool.get('party.contact_mechanism') Address = pool.get('party.address') if service.party: if service.party.addresses[0].street: addresses = service.party.addresses[0] addresses.street = service.direccion addresses.save() else: if service.direccion: party = service.party party.address = Address.create([{ 'street': service.direccion, 'party': service.party.id }]) party.save() else: service.raise_user_error( u'Actualice la dirección del cliente') if service.party.email: emails = Contact.search([('party', '=', service.party), ('type', '=', 'email')]) for email in emails: email.value = service.correo email.save() else: if service.correo: contact_mechanisms = [] contact_mechanisms.append({ 'type': 'email', 'value': service.correo, 'party': service.party.id }) contact_mechanisms = Contact.create(contact_mechanisms) else: service.raise_user_error( 'Actualice el correo del cliente') if service.party.phone: phones = Contact.search([('party', '=', service.party), ('type', '=', 'phone')]) for phone in phones: phone.value = service.telefono phone.save() else: if service.telefono: contact_mechanisms = [] contact_mechanisms.append({ 'type': 'phone', 'value': service.telefono, 'party': service.party.id, }) contact_mechanisms = Contact.create(contact_mechanisms) else: service.raise_user_error( u'Actualice el teléfono del cliente') service.raise_user_warning( 'datos%s' % service.id, u'Está seguro que los datos del cliente:\n "%s"' u'\nCorreo: "%s" y Teléfono: "%s", \nestán actualizados.', (service.party.name, service.correo, service.telefono)) service.set_number() cls.write([i for i in services if i.state != 'review'], { 'state': 'review', }) @classmethod @ModelView.button @Workflow.transition('ready') def ready(cls, services): cls.write([i for i in services if i.state != 'ready'], { 'state': 'ready', }) @classmethod @ModelView.button @Workflow.transition('without') def without(cls, services): cls.write([i for i in services if i.state != 'without'], { 'state': 'without', }) @classmethod @ModelView.button @Workflow.transition('warranty') def warranty(cls, services): cls.write([i for i in services if i.state != 'warranty'], { 'state': 'warranty', }) @classmethod @ModelView.button @Workflow.transition('delivered') def delivered(cls, services): cls.write([i for i in services if i.state != 'delivered'], { 'state': 'delivered', }) @classmethod def getTechnicalService(cls, identificacion): pool = Pool() Service = pool.get('service.service') Party = pool.get('party.party') parties = Party.search([('vat_number', '=', identificacion)]) for p in parties: party = p services = Service.search([('party', '=', party)]) all_services = [] if services: for service in services: lines_services = {} for line in service.lines: lines_services[0] = str(service.entry_date) lines_services[1] = str(service.delivery_date) lines_services[2] = service.number_service lines_services[3] = line.periferic.name lines_services[4] = line.trademark.name lines_services[5] = line.model lines_services[6] = line.failure lines_services[7] = str(line.reference_amount) lines_services[8] = line.technical.party.name lines_services[9] = service.state lines_services[10] = service.accessories lines_services[11] = service.detail all_services.append(lines_services) return all_services else: return []
class Company(metaclass=PoolMeta): __name__ = 'company.company' pem_certificate = fields.Binary('PEM Certificate') encrypted_private_key = fields.Binary('Encrypted Private Key') private_key = fields.Function(fields.Binary('Private Key'), 'get_private_key', 'set_private_key') @classmethod def get_private_key(cls, companies, name=None): converter = bytes default = None format_ = Transaction().context.pop('%s.%s' % (cls.__name__, name), '') if format_ == 'size': converter = len default = 0 pkeys = [] for company in companies: key = company._get_private_key(name) if not key: continue pkeys.append(key) if not pkeys: return {company.id: None for x in companies} return { company.id: converter(pkey) if pkey else default for (company, pkey) in zip(companies, pkeys) } def _get_private_key(self, name=None): if not self.encrypted_private_key: return None fernet = self.get_fernet_key() if not fernet: return None decrypted_key = fernet.decrypt(bytes(self.encrypted_private_key)) return decrypted_key @classmethod def set_private_key(cls, companies, name, value): encrypted_key = None if value: fernet = cls.get_fernet_key() encrypted_key = fernet.encrypt(bytes(value)) cls.write(companies, {'encrypted_private_key': encrypted_key}) @classmethod def get_fernet_key(cls): fernet_key = config.get('cryptography', 'fernet_key') if not fernet_key: _logger.error('Missing Fernet key configuration') # raise UserError(gettext('aeat_sii.msg_missing_fernet_key')) else: return Fernet(fernet_key) @contextmanager def tmp_ssl_credentials(self): if not self.pem_certificate or not self.private_key: raise UserError(gettext('aeat_sii.msg_missing_pem_cert')) with NamedTemporaryFile(suffix='.crt') as crt: with NamedTemporaryFile(suffix='.pem') as key: crt.write(self.pem_certificate) key.write(self.private_key) crt.flush() key.flush() yield (crt.name, key.name)
class Party: __name__ = 'party.party' uuid = fields.Char('UUID', required=True, help='Universally Unique Identifier') vcard = fields.Binary('VCard') @classmethod def __setup__(cls): super(Party, cls).__setup__() t = cls.__table__() cls._sql_constraints += [ ('uuid_uniq', Unique(t, t.uuid), 'The UUID of the party must be unique.'), ] @classmethod def __register__(cls, module_name): TableHandler = backend.get('TableHandler') cursor = Transaction().connection.cursor() table = TableHandler(cls, module_name) sql_table = cls.__table__() if not table.column_exist('uuid'): table.add_raw_column('uuid', cls.uuid.sql_type(), cls.uuid.sql_format, None, None) cursor.execute(*sql_table.select(sql_table.id)) for id, in cursor.fetchall(): cursor.execute(*sql_table.update(columns=[sql_table.uuid], values=[cls.default_uuid()], where=sql_table.id == id)) super(Party, cls).__register__(module_name) @staticmethod def default_uuid(): return str(uuid.uuid4()) @classmethod def create(cls, vlist): Collection = Pool().get('webdav.collection') parties = super(Party, cls).create(vlist) # Restart the cache for vcard Collection._vcard_cache.clear() return parties @classmethod def copy(cls, parties, default=None): if default is None: default = {} new_parties = [] for party in parties: current_default = default.copy() current_default['uuid'] = cls.default_uuid() new_party, = super(Party, cls).copy([party], default=current_default) new_parties.append(new_party) return new_parties @classmethod def write(cls, parties, values, *args): Collection = Pool().get('webdav.collection') super(Party, cls).write(parties, values, *args) # Restart the cache for vcard Collection._vcard_cache.clear() @classmethod def delete(cls, parties): Collection = Pool().get('webdav.collection') super(Party, cls).delete(parties) # Restart the cache for vcard Collection._vcard_cache.clear() def vcard2values(self, vcard): ''' Convert vcard to values for create or write ''' Address = Pool().get('party.address') res = {} res['name'] = vcard.fn.value if not hasattr(vcard, 'n'): vcard.add('n') vcard.n.value = vobject.vcard.Name(vcard.fn.value) res['vcard'] = vcard.serialize() if not self.id: if hasattr(vcard, 'uid'): res['uuid'] = vcard.uid.value res['addresses'] = [] to_create = [] for adr in vcard.contents.get('adr', []): vals = Address.vcard2values(adr) to_create.append(vals) if to_create: res['addresses'].append(('create', to_create)) res['contact_mechanisms'] = [] to_create = [] for email in vcard.contents.get('email', []): vals = {} vals['type'] = 'email' vals['value'] = email.value to_create.append(vals) if to_create: res['contact_mechanisms'].append(('create', to_create)) to_create = [] for tel in vcard.contents.get('tel', []): vals = {} vals['type'] = 'phone' if hasattr(tel, 'type_param') \ and 'cell' in tel.type_param.lower(): vals['type'] = 'mobile' vals['value'] = tel.value to_create.append(vals) if to_create: res['contact_mechanisms'].append(('create', to_create)) else: i = 0 res['addresses'] = [] addresses_todelete = [] for address in self.addresses: try: adr = vcard.contents.get('adr', [])[i] except IndexError: addresses_todelete.append(address.id) i += 1 continue if not hasattr(adr, 'value'): addresses_todelete.append(address.id) i += 1 continue vals = Address.vcard2values(adr) res['addresses'].append(('write', [address.id], vals)) i += 1 if addresses_todelete: res['addresses'].append(('delete', addresses_todelete)) try: new_addresses = vcard.contents.get('adr', [])[i:] except IndexError: new_addresses = [] to_create = [] for adr in new_addresses: if not hasattr(adr, 'value'): continue vals = Address.vcard2values(adr) to_create.append(vals) if to_create: res['addresses'].append(('create', to_create)) i = 0 res['contact_mechanisms'] = [] contact_mechanisms_todelete = [] for cm in self.contact_mechanisms: if cm.type != 'email': continue try: email = vcard.contents.get('email', [])[i] except IndexError: contact_mechanisms_todelete.append(cm.id) i += 1 continue vals = {} vals['value'] = email.value res['contact_mechanisms'].append(('write', cm.id, vals)) i += 1 try: new_emails = vcard.contents.get('email', [])[i:] except IndexError: new_emails = [] to_create = [] for email in new_emails: if not hasattr(email, 'value'): continue vals = {} vals['type'] = 'email' vals['value'] = email.value to_create.append(vals) if to_create: res['contact_mechanisms'].append(('create', to_create)) i = 0 for cm in self.contact_mechanisms: if cm.type not in ('phone', 'mobile'): continue try: tel = vcard.contents.get('tel', [])[i] except IndexError: contact_mechanisms_todelete.append(cm.id) i += 1 continue vals = {} vals['value'] = tel.value res['contact_mechanisms'].append(('write', cm.id, vals)) i += 1 try: new_tels = vcard.contents.get('tel', [])[i:] except IndexError: new_tels = [] to_create = [] for tel in new_tels: if not hasattr(tel, 'value'): continue vals = {} vals['type'] = 'phone' if hasattr(tel, 'type_param') \ and 'cell' in tel.type_param.lower(): vals['type'] = 'mobile' vals['value'] = tel.value to_create.append(vals) if to_create: res['contact_mechanisms'].append(('create', to_create)) if contact_mechanisms_todelete: res['contact_mechanisms'].append( ('delete', contact_mechanisms_todelete)) return res
class BinaryRequired(ModelSQL): 'Binary Required' __name__ = 'test.binary_required' binary = fields.Binary('Binary Required', required=True)
class Invoice: 'Invoice' __name__ = 'account.invoice' pos = fields.Many2One('account.pos', 'Point of Sale', on_change=['pos', 'party', 'type', 'company'], states=_POS_STATES, depends=_DEPENDS) invoice_type = fields.Many2One('account.pos.sequence', 'Invoice Type', domain=([('pos', '=', Eval('pos'))]), states=_POS_STATES, depends=_DEPENDS) pyafipws_concept = fields.Selection( [ ('1', u'1-Productos'), ('2', u'2-Servicios'), ('3', u'3-Productos y Servicios (mercado interno)'), ('4', u'4-Otros (exportación)'), ('', ''), ], 'Concepto', select=True, states={ 'readonly': Eval('state') != 'draft', 'required': Eval('pos.pos_type') == 'electronic', }, depends=['state']) pyafipws_billing_start_date = fields.Date( 'Fecha Desde', states=_BILLING_STATES, depends=_DEPENDS, help=u"Seleccionar fecha de fin de servicios - Sólo servicios") pyafipws_billing_end_date = fields.Date( 'Fecha Hasta', states=_BILLING_STATES, depends=_DEPENDS, help=u"Seleccionar fecha de inicio de servicios - Sólo servicios") pyafipws_cae = fields.Char( 'CAE', size=14, readonly=True, help=u"Código de Autorización Electrónico, devuelto por AFIP") pyafipws_cae_due_date = fields.Date( 'Vencimiento CAE', readonly=True, help=u"Fecha tope para verificar CAE, devuelto por AFIP") pyafipws_barcode = fields.Char( u'Codigo de Barras', size=40, help=u"Código de barras para usar en la impresión", readonly=True, ) pyafipws_number = fields.Char( u'Número', size=13, readonly=True, help=u"Número de factura informado a la AFIP") transactions = fields.One2Many('account_invoice_ar.afip_transaction', 'invoice', u"Transacciones", readonly=True) tipo_comprobante = fields.Selection( TIPO_COMPROBANTE, 'Comprobante', select=True, depends=['state', 'type'], states={ 'invisible': Eval('type').in_(['out_invoice', 'out_credit_note']), 'readonly': Eval('state') != 'draft', 'required': Eval('type').in_(['in_invoice', 'in_credit_note']), }) pyafipws_incoterms = fields.Selection( INCOTERMS, 'Incoterms', ) qr_imagen = fields.Binary(u'Código QR', states={ 'invisible': True, }) qr_codigo = fields.Char(u'Información Código QR', states={ 'invisible': True, }) qr_texto_modificado = fields.Char(u'Texto codificado Código QR', states={ 'invisible': True, }) pyafipws_cmp_asoc = fields.Many2Many( 'account.invoice-cmp.asoc', 'invoice', 'cmp_asoc', 'Comprobantes asociados', domain=[ ('company', '=', Eval('company', -1)), ('type', '=', 'out_invoice'), [ 'OR', ('state', 'in', ['posted', 'paid']), ('id', 'in', Eval('pyafipws_cmp_asoc')), ], ], states=_STATES, depends=_DEPENDS + ['company', 'pyafipws_cmp_asoc']) periodo_start_date = fields.Date( 'Desde periodo asociado', states=_STATES, depends=_DEPENDS, help=u"Seleccionar fecha de fin del periodo - Sólo creditos y debitos") periodo_end_date = fields.Date( 'Hasta periodo asociado', states=_STATES, depends=_DEPENDS, help= u"Seleccionar fecha de inicio del periodo - Sólo creditos y debitos") @classmethod def default_invoice_type(cls): return None @classmethod def __setup__(cls): super(Invoice, cls).__setup__() cls._buttons.update({ 'afip_post': { 'invisible': ~Eval('state').in_(['draft', 'validated']), }, }) cls._error_messages.update({ 'missing_pyafipws_billing_date': u'Debe establecer los valores "Fecha desde" y "Fecha hasta" ' \ u'en el Diario, correspondientes al servicio que se está facturando', 'invalid_invoice_number': u'El número de la factura (%d), no coincide con el que espera ' \ u'la AFIP (%d). Modifique la secuencia del diario', 'not_cae': u'No fue posible obtener el CAE. Revise las Transacciones ' \ u'para mas información', 'invalid_journal': u'Este diario (%s) no tiene establecido los datos necesaios para ' \ u'facturar electrónicamente', 'missing_sequence': u'No existe una secuencia para facturas del tipo: %s', 'too_many_sequences': u'Existe mas de una secuencia para facturas del tipo: %s', 'missing_company_iva_condition': ('The iva condition on company ' '"%(company)s" is missing.'), 'missing_party_iva_condition': ('The iva condition on party ' '"%(party)s" is missing.'), 'not_invoice_type': u'El campo «Tipo de factura» en «Factura» es requerido.', 'change_sale_configuration': u'Se debe cambiar la configuracion de la venta para procesar la factura de forma Manual.', 'missing_pyafipws_incoterms': u'Debe establecer el valor de Incoterms si desea realizar un tipo de "Factura E".', }) @classmethod @ModelView.button @Workflow.transition('validated') def validate_invoice(cls, invoices): for invoice in invoices: if invoice.type in ('out_invoice', 'out_credit_note'): invoice.check_invoice_type() super(Invoice, cls).validate(invoices) @classmethod def validate(cls, invoices): super(Invoice, cls).validate(invoices) for invoice in invoices: invoice.check_invoice_type() def check_invoice_type(self): if not self.company.party.iva_condition: self.raise_user_error('missing_company_iva_condition', { 'company': self.company.rec_name, }) if not self.party.iva_condition: self.raise_user_error('missing_party_iva_condition', { 'party': self.party.rec_name, }) if not self.invoice_type: if self.sales: self.raise_user_error('change_sale_configuration') else: if self.type in ('out_invoice', 'out_credit_note'): self.raise_user_error('not_invoice_type') def on_change_pos(self): PosSequence = Pool().get('account.pos.sequence') if not self.pos: return {'invoice_type': None} res = {} client_iva = company_iva = None if self.party: client_iva = self.party.iva_condition if self.company: company_iva = self.company.party.iva_condition if company_iva == 'responsable_inscripto': if client_iva is None: return res if client_iva == 'responsable_inscripto': kind = 'A' elif client_iva == 'consumidor_final': kind = 'B' elif self.party.vat_country is None: self.raise_user_error('unknown_country') elif self.party.vat_country == u'AR': kind = 'B' else: kind = 'E' else: kind = 'C' invoice_type, invoice_type_desc = INVOICE_TYPE_AFIP_CODE[(self.type, kind)] sequences = PosSequence.search([('pos', '=', self.pos.id), ('invoice_type', '=', invoice_type)]) if len(sequences) == 0: self.raise_user_error('missing_sequence', invoice_type_desc) elif len(sequences) > 1: self.raise_user_error('too_many_sequences', invoice_type_desc) else: res['invoice_type'] = sequences[0].id return res def set_number(self): super(Invoice, self).set_number() if self.type == 'out_invoice' or self.type == 'out_credit_note': vals = {} Sequence = Pool().get('ir.sequence') number = Sequence.get_id(self.invoice_type.invoice_sequence.id) vals['number'] = '%04d-%08d' % (self.pos.number, int(number)) self.write([self], vals) def _get_move_line(self, date, amount): res = super(Invoice, self)._get_move_line(date, amount) if self.type[:3] == 'out': res['description'] = self.party.name + u' Nro. ' + self.number else: res['description'] = self.party.name + u' Nro. ' + self.reference if self.description: res['description'] += ' / ' + self.description return res @classmethod @ModelView.button @Workflow.transition('posted') def post(cls, invoices): Move = Pool().get('account.move') moves = [] for invoice in invoices: if invoice.type == u'out_invoice' or invoice.type == u'out_credit_note': if not invoice.invoice_type: invoice.raise_user_error('not_invoice_type') if invoice.pos: if invoice.pos.pos_type == 'electronic': invoice.do_pyafipws_request_cae() if not invoice.pyafipws_cae: invoice.raise_user_error('not_cae') invoice.set_number() if invoice.type == u'out_invoice' or invoice.type == u'out_credit_note': if invoice.pos.pos_type == 'electronic': invoice.crear_codigo_qr() moves.append(invoice.create_move()) cls.write(invoices, { 'state': 'posted', }) Move.post(moves) #Bug: https://github.com/tryton-ar/account_invoice_ar/issues/38 #for invoice in invoices: # if invoice.type in ('out_invoice', 'out_credit_note'): # invoice.print_invoice() def do_pyafipws_request_cae(self): logger = logging.getLogger('pyafipws') "Request to AFIP the invoices' Authorization Electronic Code (CAE)" # if already authorized (electronic invoice with CAE), ignore if self.pyafipws_cae: logger.info(u'Se trata de obtener CAE de la factura que ya tiene. '\ u'Factura: %s, CAE: %s', self.number, self.pyafipws_cae) return # get the electronic invoice type, point of sale and service: pool = Pool() Company = pool.get('company.company') company_id = Transaction().context.get('company') if not company_id: logger.info(u'No hay companía') return company = Company(company_id) tipo_cbte = self.invoice_type.invoice_type punto_vta = self.pos.number service = self.pos.pyafipws_electronic_invoice_service # check if it is an electronic invoice sale point: ##TODO #if not tipo_cbte: # self.raise_user_error('invalid_sequence', pos.invoice_type.invoice_type) # authenticate against AFIP: auth_data = company.pyafipws_authenticate(service=service) # import the AFIP webservice helper for electronic invoice if service == 'wsfe': from pyafipws.wsfev1 import WSFEv1 # local market ws = WSFEv1() if company.pyafipws_mode_cert == 'homologacion': WSDL = "https://wswhomo.afip.gov.ar/wsfev1/service.asmx?WSDL" elif company.pyafipws_mode_cert == 'produccion': WSDL = "https://servicios1.afip.gov.ar/wsfev1/service.asmx?WSDL" #elif service == 'wsmtxca': # from pyafipws.wsmtx import WSMTXCA, SoapFault # local + detail # ws = WSMTXCA() elif service == 'wsfex': from pyafipws.wsfexv1 import WSFEXv1 # foreign trade ws = WSFEXv1() if company.pyafipws_mode_cert == 'homologacion': WSDL = "https://wswhomo.afip.gov.ar/wsfexv1/service.asmx?WSDL" elif company.pyafipws_mode_cert == 'produccion': WSDL = "https://servicios1.afip.gov.ar/wsfexv1/service.asmx?WSDL" else: logger.critical(u'WS no soportado: %s', service) return # connect to the webservice and call to the test method ws.LanzarExcepciones = True ws.Conectar(wsdl=WSDL) # set AFIP webservice credentials: ws.Cuit = company.party.vat_number ws.Token = auth_data['token'] ws.Sign = auth_data['sign'] # get the last 8 digit of the invoice number if self.move: cbte_nro = int(self.move.number[-8:]) else: Sequence = pool.get('ir.sequence') cbte_nro = int( Sequence( self.invoice_type.invoice_sequence.id).get_number_next('')) # get the last invoice number registered in AFIP if service == "wsfe" or service == "wsmtxca": cbte_nro_afip = ws.CompUltimoAutorizado(tipo_cbte, punto_vta) elif service == 'wsfex': cbte_nro_afip = ws.GetLastCMP(tipo_cbte, punto_vta) cbte_nro_next = int(cbte_nro_afip or 0) + 1 # verify that the invoice is the next one to be registered in AFIP if cbte_nro != cbte_nro_next: self.raise_user_error('invalid_invoice_number', (cbte_nro, cbte_nro_next)) # invoice number range (from - to) and date: cbte_nro = cbt_desde = cbt_hasta = cbte_nro_next if self.invoice_date: fecha_cbte = self.invoice_date.strftime("%Y-%m-%d") else: Date = pool.get('ir.date') fecha_cbte = Date.today().strftime("%Y-%m-%d") if service != 'wsmtxca': fecha_cbte = fecha_cbte.replace("-", "") # due and billing dates only for concept "services" concepto = tipo_expo = int(self.pyafipws_concept or 0) if int(concepto) != 1: payments = self.payment_term.compute(self.total_amount, self.currency) last_payment = max(payments, key=lambda x: x[0])[0] fecha_venc_pago = last_payment.strftime("%Y-%m-%d") if service != 'wsmtxca': fecha_venc_pago = fecha_venc_pago.replace("-", "") if self.pyafipws_billing_start_date: fecha_serv_desde = self.pyafipws_billing_start_date.strftime( "%Y-%m-%d") if service != 'wsmtxca': fecha_serv_desde = fecha_serv_desde.replace("-", "") else: fecha_serv_desde = None if self.pyafipws_billing_end_date: fecha_serv_hasta = self.pyafipws_billing_end_date.strftime( "%Y-%m-%d") if service != 'wsmtxca': fecha_serv_hasta = fecha_serv_hasta.replace("-", "") else: fecha_serv_hasta = None else: fecha_venc_pago = fecha_serv_desde = fecha_serv_hasta = None # customer tax number: if self.party.vat_number: nro_doc = self.party.vat_number if len(nro_doc) < 11: tipo_doc = 96 # DNI else: tipo_doc = 80 # CUIT else: nro_doc = "0" # only "consumidor final" tipo_doc = 99 # consumidor final # invoice amount totals: imp_total = str("%.2f" % abs(self.total_amount)) imp_tot_conc = "0.00" imp_neto = str("%.2f" % abs(self.untaxed_amount)) imp_iva = str("%.2f" % abs(self.tax_amount)) imp_subtotal = imp_neto # TODO: not allways the case! imp_trib = "0.00" imp_op_ex = "0.00" if self.currency.code == 'ARS': moneda_id = "PES" moneda_ctz = 1 else: moneda_id = {'USD': 'DOL'}[self.currency.code] ctz = 1 / self.currency.rate moneda_ctz = str("%.2f" % ctz) # foreign trade data: export permit, country code, etc.: if self.pyafipws_incoterms: incoterms = self.pyafipws_incoterms incoterms_ds = dict(self._fields['pyafipws_incoterms'].selection)[ self.pyafipws_incoterms] else: incoterms = incoterms_ds = None if incoterms == None and incoterms_ds == None and service == 'wsfex': self.raise_user_error('missing_pyafipws_incoterms') if int(tipo_cbte) == 19 and tipo_expo == 1: permiso_existente = "N" or "S" # not used now else: permiso_existente = "" obs_generales = self.comment if self.payment_term: forma_pago = self.payment_term.name obs_comerciales = self.payment_term.name else: forma_pago = obs_comerciales = None idioma_cbte = 1 # invoice language: spanish / español # customer data (foreign trade): nombre_cliente = self.party.name if self.party.vat_number: if self.party.vat_country == "AR": # use the Argentina AFIP's global CUIT for the country: cuit_pais_cliente = self.party.vat_number id_impositivo = None else: # use the VAT number directly id_impositivo = self.party.vat_number # TODO: the prefix could be used to map the customer country cuit_pais_cliente = None else: cuit_pais_cliente = id_impositivo = None if self.invoice_address: address = self.invoice_address domicilio_cliente = " - ".join([ address.name or '', address.street or '', address.streetbis or '', address.zip or '', address.city or '', ]) else: domicilio_cliente = "" if self.invoice_address.country: # map ISO country code to AFIP destination country code: pais_dst_cmp = { 'ar': 200, 'bo': 202, 'br': 203, 'ca': 204, 'co': 205, 'cu': 207, 'cl': 208, 'ec': 210, 'us': 212, 'mx': 218, 'py': 221, 'pe': 222, 'uy': 225, 've': 226, 'cn': 310, 'tw': 313, 'in': 315, 'il': 319, 'jp': 320, 'at': 405, 'be': 406, 'dk': 409, 'es': 410, 'fr': 412, 'gr': 413, 'it': 417, 'nl': 423, 'pt': 620, 'uk': 426, 'sz': 430, 'de': 438, 'ru': 444, 'eu': 497, 'cr': '206' }[self.invoice_address.country.code.lower()] # create the invoice internally in the helper if service == 'wsfe': ws.CrearFactura(concepto, tipo_doc, nro_doc, tipo_cbte, punto_vta, cbt_desde, cbt_hasta, imp_total, imp_tot_conc, imp_neto, imp_iva, imp_trib, imp_op_ex, fecha_cbte, fecha_venc_pago, fecha_serv_desde, fecha_serv_hasta, moneda_id, moneda_ctz) elif service == 'wsmtxca': ws.CrearFactura(concepto, tipo_doc, nro_doc, tipo_cbte, punto_vta, cbt_desde, cbt_hasta, imp_total, imp_tot_conc, imp_neto, imp_subtotal, imp_trib, imp_op_ex, fecha_cbte, fecha_venc_pago, fecha_serv_desde, fecha_serv_hasta, moneda_id, moneda_ctz, obs_generales) elif service == 'wsfex': ws.CrearFactura(tipo_cbte, punto_vta, cbte_nro, fecha_cbte, imp_total, tipo_expo, permiso_existente, pais_dst_cmp, nombre_cliente, cuit_pais_cliente, domicilio_cliente, id_impositivo, moneda_id, moneda_ctz, obs_comerciales, obs_generales, forma_pago, incoterms, idioma_cbte, incoterms_ds) # analyze VAT (IVA) and other taxes (tributo): if service in ('wsfe', 'wsmtxca'): for tax_line in self.taxes: tax = tax_line.tax if tax.group.name == "IVA": iva_id = IVA_AFIP_CODE[tax.rate] base_imp = ("%.2f" % abs(tax_line.base)) importe = ("%.2f" % abs(tax_line.amount)) # add the vat detail in the helper ws.AgregarIva(iva_id, base_imp, importe) else: if 'impuesto' in tax_line.tax.name.lower(): tributo_id = 1 # nacional elif 'iibbb' in tax_line.tax.name.lower(): tributo_id = 3 # provincial elif 'tasa' in tax_line.tax.name.lower(): tributo_id = 4 # municipal else: tributo_id = 99 desc = tax_line.name base_imp = ("%.2f" % abs(tax_line.base)) importe = ("%.2f" % abs(tax_line.amount)) alic = "%.2f" % tax_line.base # add the other tax detail in the helper ws.AgregarTributo(tributo_id, desc, base_imp, alic, importe) if (self.invoice_type.invoice_type in ('2', '3', '7', '8', '12', '13', '202', '203', '207', '208', '212', '213')): if self.periodo_start_date and self.periodo_end_date: periodo_asoc_desde = self.periodo_start_date.strftime( "%Y%m%d") periodo_asoc_hasta = self.periodo_end_date.strftime( "%Y%m%d") ws.AgregarPeriodoComprobantesAsociados( fecha_desde=periodo_asoc_desde, fecha_hasta=periodo_asoc_hasta) else: if not self.pyafipws_cmp_asoc: raise self.raise_user_error( 'Para debitos o creditos debe seleccionar el comprobante origen o el periodo asociado.' ) for cbteasoc in self.pyafipws_cmp_asoc: cbteasoc_tipo = int(cbteasoc.invoice_type.invoice_type) if cbteasoc_tipo not in INVOICE_ASOC_AFIP_CODE[ self.invoice_type.invoice_type]: raise self.raise_user_error( 'account_invoice_ar.msg_invalid_cmp_asoc') cbteasoc_nro = int(cbteasoc.number[-8:]) cbteasoc_fecha_cbte = cbteasoc.invoice_date.strftime( '%Y-%m-%d') if service != 'wsmtxca': cbteasoc_fecha_cbte = cbteasoc_fecha_cbte.replace( '-', '') ws.AgregarCmpAsoc(tipo=cbteasoc_tipo, pto_vta=punto_vta, nro=cbteasoc_nro, cuit=self.company.party.vat_number, fecha=cbteasoc_fecha_cbte) ## Agrego un item: #codigo = "PRO1" #ds = "Producto Tipo 1 Exportacion MERCOSUR ISO 9001" #qty = 2 #precio = "150.00" #umed = 1 # Ver tabla de parámetros (unidades de medida) #bonif = "50.00" #imp_total = "250.00" # importe total final del artículo # analize line items - invoice detail # umeds # Parametros. Unidades de Medida, etc. # https://code.google.com/p/pyafipws/wiki/WSFEX#WSFEX/RECEX_Parameter_Tables if service in ('wsfex', 'wsmtxca'): for line in self.lines: if line.product: codigo = line.product.code else: codigo = 0 ds = line.description qty = line.quantity umed = 7 # FIXME: (7 - unit) precio = str(line.unit_price) importe_total = str(line.amount) bonif = None # line.discount #for tax in line.taxes: # if tax.group.name == "IVA": # iva_id = IVA_AFIP_CODE[tax.rate] # imp_iva = importe * tax.rate #if service == 'wsmtxca': # ws.AgregarItem(u_mtx, cod_mtx, codigo, ds, qty, umed, # precio, bonif, iva_id, imp_iva, importe+imp_iva) if service == 'wsfex': ws.AgregarItem(codigo, ds, qty, umed, precio, importe_total, bonif) # Request the authorization! (call the AFIP webservice method) try: if service == 'wsfe': ws.CAESolicitar() vto = ws.Vencimiento elif service == 'wsmtxca': ws.AutorizarComprobante() vto = ws.Vencimiento elif service == 'wsfex': ws.Authorize(self.id) vto = ws.FchVencCAE #except SoapFault as fault: # msg = 'Falla SOAP %s: %s' % (fault.faultcode, fault.faultstring) except Exception, e: if ws.Excepcion: # get the exception already parsed by the helper #import ipdb; ipdb.set_trace() # XXX BREAKPOINT msg = ws.Excepcion + ' ' + str(e) else: # avoid encoding problem when reporting exceptions to the user: import traceback import sys msg = traceback.format_exception_only(sys.exc_type, sys.exc_value)[0] else:
class BinaryFileStorage(ModelSQL): "Binary in FileStorage" __name__ = 'test.binary_filestorage' binary = fields.Binary('Binary', file_id='binary_id') binary_id = fields.Char('Binary ID')
class ProductCategory: "Product Category extension for Nereid" __name__ = "product.category" uri = fields.Char( 'URI', select=True, on_change_with=['name', 'uri', 'parent'], states=DEFAULT_STATE2 ) displayed_on_eshop = fields.Boolean('Displayed on E-Shop?') description = fields.Text('Description') image = fields.Many2One( 'nereid.static.file', 'Image', states=DEFAULT_STATE ) image_preview = fields.Function( fields.Binary('Image Preview'), 'get_image_preview' ) sites = fields.Many2Many( 'nereid.website-product.category', 'category', 'website', 'Sites', states=DEFAULT_STATE ) @classmethod def __setup__(cls): super(ProductCategory, cls).__setup__() cls._sql_constraints += [ ('uri_uniq', 'UNIQUE(uri)', 'URI must be unique'), ] cls.per_page = 9 @staticmethod def default_displayed_on_eshop(): return True def get_image_preview(self, name=None): if self.image: return self.image.file_binary return None def on_change_with_uri(self): """Slugifies the full name of a category to make the uri on change of product name. Slugification will occur only if there is no uri filled from before. """ if self.name and not self.uri: full_name = (self.parent and self.parent.rec_name or '') \ + self.name return slugify(full_name) return self.uri @classmethod @ModelView.button def update_uri(cls, categories): """Update the uri of the category from the complete name. """ for category in categories: cls.write([category], {'uri': slugify(category.rec_name)}) @classmethod def render(cls, uri, page=1): """ Renders the template 'category.jinja' with the category and the products of the category paginated in the context :param uri: URI of the product category :param page: Integer value of the page """ ProductTemplate = Pool().get('product.template') categories = cls.search([ ('displayed_on_eshop', '=', True), ('uri', '=', uri), ('sites', '=', request.nereid_website.id) ]) if not categories: return NotFound('Product Category Not Found') # if only one category is found then it is rendered and # if more than one are found then the first one is rendered category = categories[0] products = Pagination(ProductTemplate, [ ('products.displayed_on_eshop', '=', True), ('category', '=', category.id), ], page=page, per_page=cls.per_page) return render_template( 'category.jinja', category=category, products=products ) @classmethod def render_list(cls, page=1): """ Renders the list of all categories which are displayed_on_shop=True paginated. :param page: Integer ID of the page """ categories = Pagination(cls, [ ('displayed_on_eshop', '=', True), ('sites', '=', request.nereid_website.id), ], page, cls.per_page) return render_template('category-list.jinja', categories=categories) @classmethod def get_categories(cls, page=1): """Return list of categories """ return Pagination(cls, [ ('displayed_on_eshop', '=', True), ('sites', '=', request.nereid_website.id) ], page, cls.per_page) @classmethod def get_root_categories(cls, page=1): """Return list of Root Categories.""" return Pagination(cls, [ ('displayed_on_eshop', '=', True), ('sites', '=', request.nereid_website.id), ('parent', '=', None), ], page, cls.per_page) @classmethod def context_processor(cls): """This function will be called by nereid to update the template context. Must return a dictionary that the context will be updated with. This function is registered with nereid.template.context_processor in xml code """ return { 'all_categories': cls.get_categories, 'root_categories': cls.get_root_categories, } @classmethod def sitemap_index(cls): index = SitemapIndex(cls, [ ('displayed_on_eshop', '=', True), ('id', 'in', request.nereid_website.get_categories()) ]) return index.render() @classmethod def sitemap(cls, page): sitemap_section = SitemapSection( cls, [ ('displayed_on_eshop', '=', True), ('id', 'in', request.nereid_website.get_categories()) ], page ) sitemap_section.changefreq = 'daily' return sitemap_section.render() def get_absolute_url(self, **kwargs): return url_for( 'product.category.render', uri=self.uri, **kwargs ) def _json(self): """ Return a JSON serializable dictionary of the category """ return { 'name': self.name, 'id': self.id, 'rec_name': self.rec_name, }
class ImportCSVFile(ModelSQL, ModelView): 'Import CSV File' __name__ = 'import.csv.file' profile_csv = fields.Many2One( 'import.csv', 'Profile CSV', required=True, states={'readonly': (Eval('state') != 'draft')}, depends=['state']) csv_file = fields.Binary('CSV File to import', required=True, filename='file_name', states={'readonly': (Eval('state') != 'draft')}, depends=['state']) file_name = fields.Char('File Name', required=True, states={'readonly': (Eval('state') != 'draft')}, depends=['state']) skip_repeated = fields.Boolean( 'Skip Repeated', states={'readonly': (Eval('state') != 'draft')}, depends=['state'], help='If any record of the CSV file is already imported, skip it.') update_record = fields.Boolean( 'Update Record', states={'readonly': (Eval('state') != 'draft')}, depends=['state'], help=('If any record of the CSV file is found with the search domain, ' 'update the record.')) date_ = fields.DateTime('Date', required=True) state = fields.Selection([ ('draft', 'Draft'), ('error', 'Error'), ('done', 'Done'), ], 'State', states={ 'readonly': True, }) @classmethod def __setup__(cls): super(ImportCSVFile, cls).__setup__() cls._order.insert(0, ('date_', 'DESC')) cls._order.insert(1, ('id', 'DESC')) cls._error_messages.update({ 'csv_format_error': ('Please, check that the CSV file ' 'configuration matches with the format of the CSV file.'), 'record_already_exists': ('Record %s skipped. ' 'Already exists.'), 'record_added': 'Record %s added.', 'record_updated': 'Record %s updated.', 'email_subject': 'CSV Import result', 'user_email_error': '%s has not any email address', 'import_successfully': 'Successfully imported %s records.', 'import_unsuccessfully': ('Unsuccessfully imported %s records.' 'Check configuration profile or CSV file'), }) cls._buttons.update({ 'import_file': { 'invisible': (Eval('state') == 'done'), }, }) @classmethod def default_state(cls): return 'draft' @staticmethod def default_date_(): return datetime.now() @classmethod def prepare_message(cls): User = Pool().get('res.user') user = User(Transaction().user) to_addr = user.email or config.get('email', 'from') if not to_addr: return return to_addr @classmethod def create_message(cls, from_addr, to_addrs, subject, message): msg = MIMEText(message, _charset='utf-8') msg['To'] = ', '.join(to_addrs) msg['From'] = from_addr msg['Subject'] = Header(subject, 'utf-8') return msg @classmethod def send_message(cls, message): to_addr = cls.prepare_message() if to_addr: from_addr = config.get('email', 'uri') subject = cls.raise_user_error('email_subject', raise_exception=False) msg = cls.create_message(from_addr, [to_addr], subject, message) sendmail(from_addr, [to_addr], msg) def read_csv_file(self): '''Read CSV data''' separator = self.profile_csv.separator if separator == "tab": separator = '\t' quote = self.profile_csv.quote file_ = self.csv_file # On python3 we must convert the binary file to string if hasattr(file_, 'decode'): file_ = file_.decode(self.profile_csv.character_encoding) data = StringIO(file_) if quote: rows = reader(data, delimiter=str(separator), quotechar=str(quote)) else: rows = reader(data, delimiter=str(separator)) return rows @classmethod def add_message_line(cls, csv_file, status, error_message, error_args): return ('%(time)s:\t%(profile)s ' '(%(profile_id)s)\t%(filename)s\t%(status)s\t%(message)s') % { 'time': datetime.now(), 'profile': csv_file.profile_csv.rec_name, 'profile_id': csv_file.profile_csv.id, 'filename': csv_file.file_name, 'status': status, 'message': cls.raise_user_error(error_message, error_args=error_args, raise_exception=False), } @classmethod def import_file_default(cls, csv_file): '''Default Import CSV''' pool = Pool() profile_csv = csv_file.profile_csv model = profile_csv.model has_header = profile_csv.header skip_repeated = csv_file.skip_repeated update_record = csv_file.update_record Model = pool.get(model.model) data = csv_file.read_csv_file() if has_header: next(data, None) logs = [] to_save = [] for row in data: if not row: continue domain = [] values = {} for column in profile_csv.columns: # each column, get value and assign in dict if column.constant: value = column.constant else: cells = column.column.split(',') try: vals = [row[int(c)] for c in cells if c] except IndexError: cls.raise_user_error('csv_format_error') value = column.get_value(vals) if column.add_to_domain: domain.append((column.field.name, '=', value)) values[column.field.name] = value if domain: # search record exist records = Model().search(domain, limit=1) if skip_repeated and records: logs.append( cls.add_message_line(csv_file, 'skipped', 'record_already_exists', (records[0].rec_name, ))) continue if update_record and records: record, = records # to update logs.append( cls.add_message_line(csv_file, 'done', 'record_updated', (values))) else: record = Model() # to create logs.append( cls.add_message_line(csv_file, 'done', 'record_added', (values))) else: record = Model() # to create logs.append( cls.add_message_line(csv_file, 'done', 'record_added', (values))) # assign values to object record for k, v in values.iteritems(): setattr(record, k, v) to_save.append(record) state = 'done' if to_save: try: Model.save(to_save) logs.insert( 0, cls.add_message_line(csv_file, 'done', 'import_successfully', (len(to_save), ))) except: state = 'error' logs.insert( 0, cls.add_message_line(csv_file, 'error', 'import_unsuccessfully', (len(to_save), ))) cls.write([csv_file], {'state': state}) Transaction().connection.commit() # force to commit if profile_csv.email: cls.send_message('\n'.join(logs)) @classmethod def import_file_party(cls, csv_file): '''Party Import CSV''' pool = Pool() profile_csv = csv_file.profile_csv has_header = profile_csv.header skip_repeated = csv_file.skip_repeated update_record = csv_file.update_record Party = pool.get('party.party') Address = pool.get('party.address') ContactMechanism = pool.get('party.contact_mechanism') PartyIdentifier = pool.get('party.identifier') data = csv_file.read_csv_file() if has_header: next(data, None) # Create a new list with parent and child values for each record # Group CSV lines in same record. See party.csv example in test # rows = [{ # 'record': {}, # 'domain': [], # } rows = [] for row in data: is_party = True is_address = False is_contact = False identifiers = [] domain = [] values = {} for column in profile_csv.columns: # each column, get value and assign in dict cells = column.column.split(',') cell = int(cells[0]) if column.constant: value = column.constant else: try: vals = [row[int(c)] for c in cells if c] except IndexError: cls.raise_user_error('csv_format_error') value = column.get_value(vals) if column.field.name == 'addresses' and row[cell]: is_address = True is_party = False values[column.subfield.name] = value if column.add_to_domain: domain.append( ('addresses.' + column.subfield.name, '=', value)) continue elif column.field.name == 'contact_mechanisms' and row[cell]: is_contact = True is_party = False values[column.subfield.name] = value if column.add_to_domain: domain.append( ('contact_mechanisms.' + column.subfield.name, '=', value)) continue elif column.field.name == 'identifiers' and row[cell]: identifiers.append({'code': value}) elif row[cell]: values[column.field.name] = value domain.append((column.field.name, '=', value)) # add values in rows if is_address and values: rows[-1]['record']['addresses'].append(values) if domain: rows[-1]['domain'].append(domain) elif is_contact and values: rows[-1]['record']['contact_mechanisms'].append(values) if domain: rows[-1]['domain'].append(domain) elif is_party and values: values['addresses'] = [] values['contact_mechanisms'] = [] values['identifiers'] = identifiers rows.append({ 'record': values, 'domain': domain if domain else None, }) # convert dict values to object and save logs = [] to_save = [] for row in rows: domain = row['domain'] data = row['record'] addresses = data['addresses'] del data['addresses'] contact_mechanisms = data['contact_mechanisms'] del data['contact_mechanisms'] identifiers = data['identifiers'] del data['identifiers'] # search record exist (party) if domain: records = Party.search(domain, limit=1) if skip_repeated and records: logs.append( cls.add_message_line(csv_file, 'skipped', 'record_already_exists', (records[0].rec_name, ))) continue if update_record and records: record, = records # to update logs.append( cls.add_message_line(csv_file, 'done', 'record_updated', (row))) else: record = Party() # to create logs.append( cls.add_message_line(csv_file, 'done', 'record_added', (row))) else: record = Party() # to create logs.append( cls.add_message_line(csv_file, 'done', 'record_added', (row))) # assign values to object record # (party, address, contact mechanism and identifier) for k, v in data.iteritems(): setattr(record, k, v) if not hasattr(record, 'addresses'): record.addresses = () if not hasattr(record, 'contact_mechanisms'): record.contact_mechanisms = () if not hasattr(record, 'identifiers'): record.identifiers = () addrs = () for addr in addresses: address = Address() for k, v in addr.iteritems(): setattr(address, k, v) address.on_change_country() try: # country_zip address.on_change_zip() except: pass addrs += (address, ) if addrs: record.addresses += addrs cms = () for cm in contact_mechanisms: contact = ContactMechanism() for k, v in cm.iteritems(): setattr(contact, k, v) cms += (contact, ) if cms: record.contact_mechanisms += cms idens = () for iden in identifiers: identifier = PartyIdentifier() for k, v in iden.iteritems(): setattr(identifier, k, v) idens += (identifier, ) if idens: record.identifiers += idens to_save.append(record) # to save state = 'done' if to_save: try: Party.save(to_save) logs.insert( 0, cls.add_message_line(csv_file, 'done', 'import_successfully', (len(to_save), ))) except: state = 'error' logs.insert( 0, cls.add_message_line(csv_file, 'error', 'import_unsuccessfully', (len(to_save), ))) cls.write([csv_file], {'state': state}) Transaction().connection.commit() # force to commit if profile_csv.email: cls.send_message('\n'.join(logs)) @classmethod def import_file_sale(cls, csv_file): '''Sale Import CSV''' pool = Pool() profile_csv = csv_file.profile_csv has_header = profile_csv.header skip_repeated = csv_file.skip_repeated update_record = csv_file.update_record Sale = pool.get('sale.sale') Line = pool.get('sale.line') Product = pool.get('product.product') data = csv_file.read_csv_file() if has_header: next(data, None) # Create a new list with parent and child values for each record # Group CSV lines in same record. See party.csv example in test # rows = [{ # 'record': {}, # 'domain': [], # } rows = [] for row in data: is_sale = True is_line = False domain = [] values = {} for column in profile_csv.columns: # each column, get value and assign in dict cells = column.column.split(',') cell = int(cells[0]) if column.constant: value = column.constant else: try: vals = [row[int(c)] for c in cells if c] except IndexError: cls.raise_user_error('csv_format_error') value = column.get_value(vals) if column.field.name == 'line' and row[cell]: is_line = True is_sale = False if column.subfield.name == 'product': products = Product.search([ ('rec_name', '=', value), ]) if products: values[column.subfield.name] = value else: values['description'] = value if column.add_to_domain: domain.append( ('lines.' + column.subfield.name, '=', value)) continue elif row[cell]: values[column.field.name] = value domain.append((column.field.name, '=', value)) # add values in rows if is_line and values: rows[-1]['record']['lines'].append(values) if domain: rows[-1]['domain'].append(domain) elif is_sale and values: values['lines'] = [] rows.append({ 'record': values, 'domain': domain if domain else None, }) # convert dict values to object and save logs = [] to_save = [] for row in rows: domain = row['domain'] data = row['record'] lines = data['lines'] del data['lines'] # search record exist (party) if domain: records = Sale.search(domain, limit=1) if skip_repeated and records: logs.append( cls.add_message_line(csv_file, 'skipped', 'record_already_exists', (records[0].rec_name, ))) continue if update_record and records: record, = records # to update logs.append( cls.add_message_line(csv_file, 'done', 'record_updated', (row))) else: record = Sale() # to create logs.append( cls.add_message_line(csv_file, 'done', 'record_added', (row))) else: record = Sale() # to create logs.append( cls.add_message_line(csv_file, 'done', 'record_added', (row))) # assign values to object record # (sale and line) for k, v in data.iteritems(): setattr(record, k, v) if not hasattr(record, 'lines'): record.lines = () sale_lines = () for l in lines: line = Line() for k, v in l.iteritems(): setattr(line, k, v) line.on_change_product() sale_lines += (line, ) if sale_lines: record.lines += sale_lines to_save.append(record) # to save state = 'done' if to_save: try: Sale.save(to_save) logs.insert( 0, cls.add_message_line(csv_file, 'done', 'import_successfully', (len(to_save), ))) except: state = 'error' logs.insert( 0, cls.add_message_line(csv_file, 'error', 'import_unsuccessfully', (len(to_save), ))) cls.write([csv_file], {'state': state}) Transaction().connection.commit() # force to commit if profile_csv.email: cls.send_message('\n'.join(logs)) @classmethod @ModelView.button def import_file(cls, csv_files): '''Import CSV''' for csv_file in csv_files: profile_csv = csv_file.profile_csv import_csv = getattr(cls, 'import_file_%s' % profile_csv.method) import_csv(csv_file)
class Statement(Workflow, ModelSQL, ModelView): 'Account Statement' __name__ = 'account.statement' name = fields.Char('Name', required=True) company = fields.Many2One( 'company.company', 'Company', required=True, select=True, states=_STATES, domain=[ ('id', If(Eval('context', {}).contains('company'), '=', '!='), Eval('context', {}).get('company', -1)), ], depends=_DEPENDS) journal = fields.Many2One('account.statement.journal', 'Journal', required=True, select=True, domain=[ ('company', '=', Eval('company', -1)), ], states={ 'readonly': (Eval('state') != 'draft') | Eval('lines', [0]), }, depends=['state', 'company']) currency_digits = fields.Function(fields.Integer('Currency Digits'), 'on_change_with_currency_digits') date = fields.Date('Date', required=True, select=True) start_balance = fields.Numeric('Start Balance', digits=(16, Eval('currency_digits', 2)), states=_BALANCE_STATES, depends=_BALANCE_DEPENDS + ['currency_digits']) end_balance = fields.Numeric('End Balance', digits=(16, Eval('currency_digits', 2)), states=_BALANCE_STATES, depends=_BALANCE_DEPENDS + ['currency_digits']) balance = fields.Function( fields.Numeric('Balance', digits=(16, Eval('currency_digits', 2)), states=_BALANCE_STATES, depends=_BALANCE_DEPENDS + ['currency_digits']), 'on_change_with_balance') total_amount = fields.Numeric('Total Amount', digits=(16, Eval('currency_digits', 2)), states=_AMOUNT_STATES, depends=_AMOUNT_DEPENDS + ['currency_digits']) number_of_lines = fields.Integer('Number of Lines', states=_NUMBER_STATES, depends=_NUMBER_DEPENDS) lines = fields.One2Many('account.statement.line', 'statement', 'Lines', states={ 'readonly': (Eval('state') != 'draft') | ~Eval('journal'), }, depends=['state', 'journal']) origins = fields.One2Many('account.statement.origin', 'statement', "Origins", states={ 'readonly': Eval('state') != 'draft', }, depends=['state']) origin_file = fields.Binary("Origin File", readonly=True, file_id=file_id, store_prefix=store_prefix) origin_file_id = fields.Char("Origin File ID", readonly=True) state = fields.Selection(STATES, 'State', readonly=True, select=True) validation = fields.Function(fields.Char('Validation'), 'on_change_with_validation') @classmethod def __setup__(cls): super(Statement, cls).__setup__() cls._order[0] = ('id', 'DESC') cls._error_messages.update({ 'wrong_end_balance': 'End Balance must be "%s".', 'wrong_total_amount': 'Total Amount must be "%s".', 'wrong_number_of_lines': 'Number of Lines must be "%s".', 'delete_cancel': ('Statement "%s" must be cancelled before ' 'deletion.'), 'paid_invoice_draft_statement': ('There are paid invoices on ' 'draft statements.'), 'debit_credit_account_statement_journal': ('Please provide ' 'debit and credit account on statement journal "%s".'), 'post_with_pending_amount': ('Origin line "%(origin)s" ' 'of statement "%(statement)s" still has a pending amount ' 'of "%(amount)s".'), }) cls._transitions |= set(( ('draft', 'validated'), ('draft', 'cancel'), ('validated', 'posted'), ('validated', 'cancel'), ('cancel', 'draft'), )) cls._buttons.update({ 'draft': { 'invisible': Eval('state') != 'cancel', }, 'validate_statement': { 'invisible': Eval('state') != 'draft', }, 'post': { 'invisible': Eval('state') != 'validated', }, 'cancel': { 'invisible': ~Eval('state').in_(['draft', 'validated']), }, }) @classmethod def __register__(cls, module_name): TableHandler = backend.get('TableHandler') transaction = Transaction() cursor = transaction.connection.cursor() sql_table = cls.__table__() # Migration from 1.8: new field company table = TableHandler(cls, module_name) company_exist = table.column_exist('company') super(Statement, cls).__register__(module_name) # Migration from 1.8: fill new field company if not company_exist: offset = 0 limit = transaction.database.IN_MAX statements = True while statements: statements = cls.search([], offset=offset, limit=limit) offset += limit for statement in statements: cls.write([statement], { 'company': statement.journal.company.id, }) table = TableHandler(cls, module_name) table.not_null_action('company', action='add') # Migration from 3.2: remove required on start/end balance table.not_null_action('start_balance', action='remove') table.not_null_action('end_balance', action='remove') # Migration from 3.2: add required name cursor.execute(*sql_table.update( [sql_table.name], [sql_table.id.cast(cls.name.sql_type().base)], where=sql_table.name == Null)) @staticmethod def default_company(): return Transaction().context.get('company') @staticmethod def default_state(): return 'draft' @staticmethod def default_date(): Date = Pool().get('ir.date') return Date.today() @staticmethod def default_currency_digits(): Company = Pool().get('company.company') if Transaction().context.get('company'): company = Company(Transaction().context['company']) return company.currency.digits return 2 @fields.depends('journal', 'state', 'lines') def on_change_journal(self): if not self.journal: return statements = self.search([ ('journal', '=', self.journal.id), ], order=[ ('date', 'DESC'), ('id', 'DESC'), ], limit=1) if not statements: return statement, = statements self.start_balance = statement.end_balance @fields.depends('journal') def on_change_with_currency_digits(self, name=None): if self.journal: return self.journal.currency.digits return 2 def get_end_balance(self, name): end_balance = self.start_balance for line in self.lines: end_balance += line.amount return end_balance @fields.depends('start_balance', 'end_balance') def on_change_with_balance(self, name=None): return ((getattr(self, 'end_balance', 0) or 0) - (getattr(self, 'start_balance', 0) or 0)) @fields.depends('lines', 'journal') def on_change_lines(self): pool = Pool() Currency = pool.get('currency.currency') Line = pool.get('account.statement.line') if self.journal and self.lines: invoices = set() for line in self.lines: if getattr(line, 'invoice', None): invoices.add(line.invoice) invoice_id2amount_to_pay = {} for invoice in invoices: with Transaction().set_context(date=invoice.currency_date): if invoice.type == 'out': sign = -1 else: sign = 1 invoice_id2amount_to_pay[ invoice.id] = sign * (Currency.compute( invoice.currency, invoice.amount_to_pay, self.journal.currency)) lines = list(self.lines) line_offset = 0 for index, line in enumerate(self.lines or []): if getattr(line, 'invoice', None) and line.id: amount_to_pay = invoice_id2amount_to_pay[line.invoice.id] if (not self.journal.currency.is_zero(amount_to_pay) and getattr(line, 'amount', None) and (line.amount >= 0) == (amount_to_pay <= 0)): if abs(line.amount) > abs(amount_to_pay): new_line = Line() for field_name, field in Line._fields.iteritems(): if field_name == 'id': continue try: setattr(new_line, field_name, getattr(line, field_name)) except AttributeError: pass new_line.amount = line.amount + amount_to_pay new_line.invoice = None line_offset += 1 lines.insert(index + line_offset, new_line) invoice_id2amount_to_pay[line.invoice.id] = 0 line.amount = amount_to_pay.copy_sign(line.amount) else: invoice_id2amount_to_pay[line.invoice.id] = ( line.amount + amount_to_pay) else: line.invoice = None self.lines = lines @fields.depends('journal') def on_change_with_validation(self, name=None): if self.journal: return self.journal.validation def _group_key(self, line): key = ( ('number', line.number or Unequal()), ('date', line.date), ('party', line.party), ) return key def _get_grouped_line(self): "Return Line class for grouped lines" assert self.lines keys = [k[0] for k in self._group_key(self.lines[0])] class Line(namedtuple('Line', keys + ['lines'])): @property def amount(self): return sum((l.amount for l in self.lines)) @property def descriptions(self): done = set() for line in self.lines: if line.description and line.description not in done: done.add(line.description) yield line.description return Line @property def grouped_lines(self): if self.origins: for origin in self.origins: yield origin elif self.lines: Line = self._get_grouped_line() for key, lines in groupby(self.lines, key=self._group_key): yield Line(**dict(key + (('lines', list(lines)), ))) @classmethod def delete(cls, statements): # Cancel before delete cls.cancel(statements) for statement in statements: if statement.state != 'cancel': cls.raise_user_error('delete_cancel', (statement.rec_name, )) super(Statement, cls).delete(statements) @classmethod @ModelView.button @Workflow.transition('draft') def draft(cls, statements): pass def validate_balance(self): pool = Pool() Lang = pool.get('ir.lang') end_balance = (self.start_balance + sum(l.amount for l in self.lines)) if end_balance != self.end_balance: lang, = Lang.search([ ('code', '=', Transaction().language), ]) amount = Lang.format( lang, '%.' + str(self.journal.currency.digits) + 'f', end_balance, True) self.raise_user_error('wrong_end_balance', amount) def validate_amount(self): pool = Pool() Lang = pool.get('ir.lang') amount = sum(l.amount for l in self.lines) if amount != self.total_amount: lang, = Lang.search([ ('code', '=', Transaction().language), ]) amount = Lang.format( lang, '%.' + str(self.journal.currency.digits) + 'f', amount, True) self.raise_user_error('wrong_total_amount', amount) def validate_number_of_lines(self): number = len(list(self.grouped_lines)) if number != self.number_of_lines: self.raise_user_error('wrong_number_of_lines', number) @classmethod @ModelView.button @Workflow.transition('validated') def validate_statement(cls, statements): pool = Pool() Line = pool.get('account.statement.line') for statement in statements: getattr(statement, 'validate_%s' % statement.validation)() cls.create_move(statements) cls.write(statements, { 'state': 'validated', }) common_lines = Line.search([ ('statement.state', '=', 'draft'), ('invoice.state', '=', 'paid'), ]) if common_lines: warning_key = '_'.join(str(l.id) for l in common_lines) cls.raise_user_warning(warning_key, 'paid_invoice_draft_statement') Line.write(common_lines, { 'invoice': None, }) @classmethod def create_move(cls, statements): '''Create move for the statements and try to reconcile the lines. Returns the list of move, statement and lines ''' pool = Pool() Line = pool.get('account.statement.line') Move = pool.get('account.move') MoveLine = pool.get('account.move.line') moves = [] for statement in statements: for key, lines in groupby(statement.lines, key=statement._group_key): lines = list(lines) key = dict(key) move = statement._get_move(key) moves.append((move, statement, lines)) Move.save([m for m, _, _ in moves]) to_write = [] for move, _, lines in moves: to_write.append(lines) to_write.append({ 'move': move.id, }) if to_write: Line.write(*to_write) move_lines = [] for move, statement, lines in moves: amount = 0 amount_second_currency = 0 for line in lines: move_line = line.get_move_line() move_line.move = move amount += move_line.debit - move_line.credit if move_line.amount_second_currency: amount_second_currency += move_line.amount_second_currency move_lines.append((move_line, line)) move_line = statement._get_move_line(amount, amount_second_currency, lines) move_line.move = move move_lines.append((move_line, None)) MoveLine.save([l for l, _ in move_lines]) for move_line, line in move_lines: if line: line.reconcile(move_line) return moves def _get_move(self, key): 'Return Move for the grouping key' pool = Pool() Move = pool.get('account.move') Period = pool.get('account.period') period_id = Period.find(self.company.id, date=key['date']) return Move( period=period_id, journal=self.journal.journal, date=key['date'], origin=self, company=self.company, description=unicode(key['number']), ) def _get_move_line(self, amount, amount_second_currency, lines): 'Return counterpart Move Line for the amount' pool = Pool() MoveLine = pool.get('account.move.line') if amount < 0: account = self.journal.journal.debit_account else: account = self.journal.journal.credit_account if not account: self.raise_user_error('debit_credit_account_statement_journal', (self.journal.rec_name, )) if self.journal.currency != self.company.currency: second_currency = self.journal.currency amount_second_currency *= -1 else: second_currency = None amount_second_currency = None descriptions = {l.description for l in lines} if len(descriptions) == 1: description, = descriptions else: description = '' return MoveLine( debit=abs(amount) if amount < 0 else 0, credit=abs(amount) if amount > 0 else 0, account=account, second_currency=second_currency, amount_second_currency=amount_second_currency, description=description, ) @classmethod @ModelView.button @Workflow.transition('posted') def post(cls, statements): StatementLine = Pool().get('account.statement.line') for statement in statements: for origin in statement.origins: if origin.pending_amount: cls.raise_user_error( 'post_with_pending_amount', { 'origin': origin.rec_name, 'amount': origin.pending_amount, 'statement': statement.rec_name, }) lines = [l for s in statements for l in s.lines] StatementLine.post_move(lines) @classmethod @ModelView.button @Workflow.transition('cancel') def cancel(cls, statements): StatementLine = Pool().get('account.statement.line') lines = [l for s in statements for l in s.lines] StatementLine.delete_move(lines)
class LoadPKCS12Start(ModelView): "Load PKCS12 Start" __name__ = "aeat.sii.load_pkcs12.start" pfx = fields.Binary('PFX File', required=True) password = fields.Char('Password', required=True)
class ActionReport(metaclass=PoolMeta): __name__ = 'ir.action.report' html_template = fields.Many2One('html.template', 'Template', domain=[ ('type', 'in', ['base', 'extension']), ], states={ 'invisible': Eval('template_extension') != 'jinja', }, depends=['template_extension']) html_header_template = fields.Many2One( 'html.template', 'Header', domain=[ ('type', '=', 'header'), ], states={ 'invisible': Eval('template_extension') != 'jinja', }, depends=['template_extension']) html_footer_template = fields.Many2One( 'html.template', 'Footer', domain=[ ('type', '=', 'footer'), ], states={ 'invisible': Eval('template_extension') != 'jinja', }, depends=['template_extension']) html_last_footer_template = fields.Many2One( 'html.template', 'Last Footer', domain=[ ('type', '=', 'footer'), ], states={ 'invisible': Eval('template_extension') != 'jinja', }, depends=['template_extension']) html_templates = fields.One2Many('html.report.template', 'report', 'Templates', states={ 'invisible': Eval('template_extension') != 'jinja', }, depends=['template_extension']) html_content = fields.Function( fields.Text('Content', states={ 'invisible': Eval('template_extension') != 'jinja', }, depends=['template_extension']), 'get_content') html_translations = fields.One2Many('html.template.translation', 'report', 'Translations') _html_translation_cache = Cache('html.template.translation', size_limit=10240, context=False) html_header_content = fields.Function(fields.Binary('Header Content'), 'get_content') html_footer_content = fields.Function(fields.Binary('Footer Content'), 'get_content') html_last_footer_content = fields.Function( fields.Binary('Last Footer Content'), 'get_content') @classmethod def __setup__(cls): super(ActionReport, cls).__setup__() jinja_option = ('jinja', 'Jinja') if jinja_option not in cls.template_extension.selection: cls.template_extension.selection.append(jinja_option) @classmethod def view_attributes(cls): return super(ActionReport, cls).view_attributes() + [ ('//page[@id="html_report"]', 'states', { 'invisible': Eval('template_extension') != 'jinja', }) ] def get_content(self, name): obj_name = name.replace('content', 'template') obj = getattr(self, obj_name) if not obj: return content = [] for template in self.html_templates: if template.template_used and template.template_used.all_content: content.append(template.template_used.all_content or '') content.append(obj.all_content or '') return '\n\n'.join(content) @classmethod def validate(cls, reports): for report in reports: report.check_template_jinja() def check_template_jinja(self): if self.template_extension == 'jinja': return missing, unused = self.get_missing_unused_signatures() if missing: raise UserError( gettext( 'html_report.missing_signatures', { 'template': self.rec_name, 'missing': '\n'.join( sorted([x.rec_name for x in missing])) })) if unused: raise UserError( gettext( 'html_report.unused_signatures', { 'template': self.rec_name, 'unused': '\n'.join( sorted([x.rec_name for x in unused])) })) def get_missing_unused_signatures(self): existing = {x.signature for x in self.html_templates} required = self.required_signatures() missing = required - existing unused = existing - required return missing, unused def required_signatures(self): if not self.html_template: return set() signatures = {x for x in self.html_template.uses} for template in self.html_templates: if not template.template: continue signatures |= {x for x in template.template.uses} return signatures @fields.depends('html_template', 'html_templates') def on_change_html_template(self): pool = Pool() Template = pool.get('html.template') ReportTemplate = pool.get('html.report.template') missing, unused = self.get_missing_unused_signatures() templates = list(self.html_templates) for signature in missing: record = ReportTemplate() record.signature = signature implementors = Template.search([('implements', '=', signature)]) if len(implementors) == 1: record.template, = implementors templates.append(record) self.html_templates = templates @fields.depends('html_template', 'html_templates') def on_change_html_header_template(self): pool = Pool() Template = pool.get('html.template') ReportTemplate = pool.get('html.report.template') missing, unused = self.get_missing_unused_signatures() templates = list(self.html_templates) for signature in missing: record = ReportTemplate() record.signature = signature implementors = Template.search([('implements', '=', signature)]) if len(implementors) == 1: record.template, = implementors templates.append(record) self.html_templates = templates @classmethod def gettext(cls, *args, **variables): HTMLTemplateTranslation = Pool().get('html.template.translation') report, src, lang = args key = (report, src, lang) text = cls._html_translation_cache.get(key) if text is None: translations = HTMLTemplateTranslation.search([ ('report', '=', report), ('src', '=', src), ('lang', '=', lang), ], limit=1) if translations: text = translations[0].value else: text = src cls._html_translation_cache.set(key, text) return text if not variables else text % variables
class ActionReport(ActionMixin, ModelSQL, ModelView): "Action report" __name__ = 'ir.action.report' _action_name = 'report_name' model = fields.Char('Model') report_name = fields.Char('Internal Name', required=True) report = fields.Char("Path", states={ 'invisible': Eval('is_custom', False), }, depends=['is_custom']) report_content_custom = fields.Binary('Content') is_custom = fields.Function(fields.Boolean("Is Custom"), 'get_is_custom') report_content = fields.Function(fields.Binary( 'Content', filename='report_content_name'), 'get_report_content', setter='set_report_content') report_content_name = fields.Function( fields.Char('Content Name'), 'on_change_with_report_content_name') report_content_html = fields.Function(fields.Binary( "Content HTML", states={ 'invisible': ~Eval('template_extension').in_(['html', 'xhtml']), }, depends=['template_extension']), 'get_report_content_html', setter='set_report_content_html') action = fields.Many2One('ir.action', 'Action', required=True, ondelete='CASCADE') direct_print = fields.Boolean('Direct Print') single = fields.Boolean( "Single", help="Check if the template works only for one record.") translatable = fields.Boolean( "Translatable", help="Uncheck to disable translations for this report.") template_extension = fields.Selection([ ('odt', 'OpenDocument Text'), ('odp', 'OpenDocument Presentation'), ('ods', 'OpenDocument Spreadsheet'), ('odg', 'OpenDocument Graphics'), ('txt', 'Plain Text'), ('xml', 'XML'), ('html', 'HTML'), ('xhtml', 'XHTML'), ], string='Template Extension', required=True, translate=False) extension = fields.Selection( [ ('', ''), ('bib', 'BibTex'), ('bmp', 'Windows Bitmap'), ('csv', 'Text CSV'), ('dbf', 'dBase'), ('dif', 'Data Interchange Format'), ('doc', 'Microsoft Word 97/2000/XP'), ('doc6', 'Microsoft Word 6.0'), ('doc95', 'Microsoft Word 95'), ('docbook', 'DocBook'), ('docx', 'Microsoft Office Open XML Text'), ('docx7', 'Microsoft Word 2007 XML'), ('emf', 'Enhanced Metafile'), ('eps', 'Encapsulated PostScript'), ('gif', 'Graphics Interchange Format'), ('html', 'HTML Document'), ('jpg', 'Joint Photographic Experts Group'), ('met', 'OS/2 Metafile'), ('ooxml', 'Microsoft Office Open XML'), ('pbm', 'Portable Bitmap'), ('pct', 'Mac Pict'), ('pdb', 'AportisDoc (Palm)'), ('pdf', 'Portable Document Format'), ('pgm', 'Portable Graymap'), ('png', 'Portable Network Graphic'), ('ppm', 'Portable Pixelmap'), ('ppt', 'Microsoft PowerPoint 97/2000/XP'), ('psw', 'Pocket Word'), ('pwp', 'PlaceWare'), ('pxl', 'Pocket Excel'), ('ras', 'Sun Raster Image'), ('rtf', 'Rich Text Format'), ('latex', 'LaTeX 2e'), ('sda', 'StarDraw 5.0 (OpenOffice.org Impress)'), ('sdc', 'StarCalc 5.0'), ('sdc4', 'StarCalc 4.0'), ('sdc3', 'StarCalc 3.0'), ('sdd', 'StarImpress 5.0'), ('sdd3', 'StarDraw 3.0 (OpenOffice.org Impress)'), ('sdd4', 'StarImpress 4.0'), ('sdw', 'StarWriter 5.0'), ('sdw4', 'StarWriter 4.0'), ('sdw3', 'StarWriter 3.0'), ('slk', 'SYLK'), ('svg', 'Scalable Vector Graphics'), ('svm', 'StarView Metafile'), ('swf', 'Macromedia Flash (SWF)'), ('sxc', 'OpenOffice.org 1.0 Spreadsheet'), ('sxi', 'OpenOffice.org 1.0 Presentation'), ('sxd', 'OpenOffice.org 1.0 Drawing'), ('sxd3', 'StarDraw 3.0'), ('sxd5', 'StarDraw 5.0'), ('sxw', 'Open Office.org 1.0 Text Document'), ('text', 'Text Encoded'), ('tiff', 'Tagged Image File Format'), ('txt', 'Plain Text'), ('wmf', 'Windows Metafile'), ('xhtml', 'XHTML Document'), ('xls', 'Microsoft Excel 97/2000/XP'), ('xls5', 'Microsoft Excel 5.0'), ('xls95', 'Microsoft Excel 95'), ('xlsx', 'Microsoft Excel 2007/2010 XML'), ('xpm', 'X PixMap'), ], translate=False, string='Extension', help='Leave empty for the same as template, ' 'see LibreOffice documentation for compatible format.') module = fields.Char('Module', readonly=True, select=True) _template_cache = MemoryCache('ir.action.report.template', context=False) @classmethod def __register__(cls, module_name): super(ActionReport, cls).__register__(module_name) transaction = Transaction() cursor = transaction.connection.cursor() table = cls.__table_handler__(module_name) action_report = cls.__table__() # Migration from 3.4 remove report_name_module_uniq constraint table.drop_constraint('report_name_module_uniq') # Migration from 4.4 replace plain extension by txt cursor.execute( *action_report.update([action_report.extension], ['txt'], where=action_report.extension == 'plain')) @staticmethod def default_type(): return 'ir.action.report' @staticmethod def default_report_content(): return None @staticmethod def default_direct_print(): return False @classmethod def default_single(cls): return False @classmethod def default_translatable(cls): return True @staticmethod def default_template_extension(): return 'odt' @staticmethod def default_extension(): return '' @staticmethod def default_module(): return Transaction().context.get('module') or '' def get_is_custom(self, name): return bool(self.report_content_custom) @classmethod def get_report_content(cls, reports, name): contents = {} converter = fields.Binary.cast default = None format_ = Transaction().context.get('%s.%s' % (cls.__name__, name), '') if format_ == 'size': converter = len default = 0 for report in reports: data = getattr(report, name + '_custom') if not data and getattr(report, name[:-8]): try: with file_open(getattr(report, name[:-8]).replace('/', os.sep), mode='rb') as fp: data = fp.read() except Exception: data = None contents[report.id] = converter(data) if data else default return contents @classmethod def set_report_content(cls, records, name, value): cls.write(records, {'%s_custom' % name: value}) @classmethod def get_report_content_html(cls, reports, name): return cls.get_report_content(reports, name[:-5]) @classmethod def set_report_content_html(cls, reports, name, value): if value is not None: value = value.encode('utf-8') cls.set_report_content(reports, name[:-5], value) @fields.depends('name', 'template_extension') def on_change_with_report_content_name(self, name=None): return ''.join( filter(None, [self.name, os.extsep, self.template_extension])) @classmethod def get_pyson(cls, reports, name): pysons = {} field = name[6:] defaults = {} for report in reports: pysons[report.id] = (getattr(report, field) or defaults.get(field, 'null')) return pysons @classmethod def copy(cls, reports, default=None): if default is None: default = {} default = default.copy() default.setdefault('module', None) new_reports = [] for report in reports: if report.report: default['report_content'] = None default['report'] = None default['report_name'] = report.report_name new_reports.extend( super(ActionReport, cls).copy([report], default=default)) return new_reports @classmethod def write(cls, reports, values, *args): context = Transaction().context if 'module' in context: actions = iter((reports, values) + args) args = [] for reports, values in zip(actions, actions): values = values.copy() values['module'] = context['module'] args.extend((reports, values)) reports, values = args[:2] args = args[2:] cls._template_cache.clear() super(ActionReport, cls).write(reports, values, *args) def get_template_cached(self): return self._template_cache.get(self.id) def set_template_cached(self, template): self._template_cache.set(self.id, template)
class Statement(Workflow, ModelSQL, ModelView): 'Account Statement' __name__ = 'account.statement' _states = {'readonly': Eval('state') != 'draft'} _balance_states = _states.copy() _balance_states.update({ 'invisible': ~Eval('validation', '').in_(['balance']), 'required': Eval('validation', '').in_(['balance']), }) _amount_states = _states.copy() _amount_states.update({ 'invisible': ~Eval('validation', '').in_(['amount']), 'required': Eval('validation', '').in_(['amount']), }) _number_states = _states.copy() _number_states.update({ 'invisible': ~Eval('validation', '').in_(['number_of_lines']), 'required': Eval('validation', '').in_(['number_of_lines']), }) name = fields.Char('Name', required=True) company = fields.Many2One( 'company.company', "Company", required=True, select=True, states=_states) journal = fields.Many2One('account.statement.journal', 'Journal', required=True, select=True, domain=[ ('company', '=', Eval('company', -1)), ], states={ 'readonly': (Eval('state') != 'draft') | Eval('lines', [0]), }) currency = fields.Function(fields.Many2One( 'currency.currency', "Currency"), 'on_change_with_currency') date = fields.Date('Date', required=True, select=True) start_balance = Monetary( "Start Balance", currency='currency', digits='currency', states=_balance_states) end_balance = Monetary( "End Balance", currency='currency', digits='currency', states=_balance_states) balance = fields.Function(Monetary( "Balance", currency='currency', digits='currency', states=_balance_states), 'on_change_with_balance') total_amount = Monetary( "Total Amount", currency='currency', digits='currency', states=_amount_states) number_of_lines = fields.Integer('Number of Lines', states=_number_states) lines = fields.One2Many('account.statement.line', 'statement', 'Lines', states={ 'readonly': (Eval('state') != 'draft') | ~Eval('journal'), }) origins = fields.One2Many('account.statement.origin', 'statement', "Origins", states={ 'readonly': Eval('state') != 'draft', }) origin_file = fields.Binary( "Origin File", readonly=True, file_id=file_id, store_prefix=store_prefix) origin_file_id = fields.Char("Origin File ID", readonly=True) state = fields.Selection([ ('draft', "Draft"), ('validated', "Validated"), ('cancelled', "Cancelled"), ('posted', "Posted"), ], "State", readonly=True, select=True, sort=False) validation = fields.Function(fields.Char('Validation'), 'on_change_with_validation') to_reconcile = fields.Function( fields.Boolean("To Reconcile"), 'get_to_reconcile') del _states del _balance_states del _amount_states del _number_states @classmethod def __setup__(cls): super(Statement, cls).__setup__() cls._order[0] = ('id', 'DESC') cls._transitions |= set(( ('draft', 'validated'), ('draft', 'cancelled'), ('validated', 'posted'), ('validated', 'cancelled'), ('cancelled', 'draft'), )) cls._buttons.update({ 'draft': { 'invisible': Eval('state') != 'cancelled', 'depends': ['state'], }, 'validate_statement': { 'invisible': Eval('state') != 'draft', 'depends': ['state'], }, 'post': { 'invisible': Eval('state') != 'validated', 'depends': ['state'], }, 'cancel': { 'invisible': ~Eval('state').in_(['draft', 'validated']), 'depends': ['state'], }, 'reconcile': { 'invisible': Eval('state').in_(['draft', 'cancelled']), 'readonly': ~Eval('to_reconcile'), 'depends': ['state', 'to_reconcile'], }, }) cls.__rpc__.update({ 'post': RPC( readonly=False, instantiate=0, fresh_session=True), }) @classmethod def __register__(cls, module_name): transaction = Transaction() cursor = transaction.connection.cursor() sql_table = cls.__table__() super(Statement, cls).__register__(module_name) table = cls.__table_handler__(module_name) # Migration from 3.2: remove required on start/end balance table.not_null_action('start_balance', action='remove') table.not_null_action('end_balance', action='remove') # Migration from 3.2: add required name cursor.execute(*sql_table.update([sql_table.name], [sql_table.id.cast(cls.name.sql_type().base)], where=sql_table.name == Null)) # Migration from 5.6: rename state cancel to cancelled cursor.execute(*sql_table.update( [sql_table.state], ['cancelled'], where=sql_table.state == 'cancel')) @staticmethod def default_company(): return Transaction().context.get('company') @staticmethod def default_state(): return 'draft' @staticmethod def default_date(): Date = Pool().get('ir.date') return Date.today() @fields.depends('journal', 'state', 'lines') def on_change_journal(self): if not self.journal: return statements = self.search([ ('journal', '=', self.journal.id), ], order=[ ('date', 'DESC'), ('id', 'DESC'), ], limit=1) if not statements: return statement, = statements self.start_balance = statement.end_balance @fields.depends('journal') def on_change_with_currency(self, name=None): if self.journal: return self.journal.currency.id @fields.depends('start_balance', 'end_balance') def on_change_with_balance(self, name=None): return ((getattr(self, 'end_balance', 0) or 0) - (getattr(self, 'start_balance', 0) or 0)) @fields.depends('origins', 'lines', 'journal', 'company') def on_change_origins(self): if not self.journal or not self.origins or not self.company: return if self.journal.currency != self.company.currency: return invoices = set() for line in self.lines: if (line.invoice and line.invoice.currency == self.company.currency): invoices.add(line.invoice) for origin in self.origins: for line in origin.lines: if (line.invoice and line.invoice.currency == self.company.currency): invoices.add(line.invoice) invoice_id2amount_to_pay = {} for invoice in invoices: if invoice.type == 'out': sign = -1 else: sign = 1 invoice_id2amount_to_pay[invoice.id] = sign * invoice.amount_to_pay origins = list(self.origins) for origin in origins: lines = list(origin.lines) for line in lines: if (line.invoice and line.id and line.invoice.id in invoice_id2amount_to_pay): amount_to_pay = invoice_id2amount_to_pay[line.invoice.id] if (amount_to_pay and getattr(line, 'amount', None) and (line.amount >= 0) == (amount_to_pay <= 0)): if abs(line.amount) > abs(amount_to_pay): line.amount = amount_to_pay.copy_sign(line.amount) else: invoice_id2amount_to_pay[line.invoice.id] = ( line.amount + amount_to_pay) else: line.invoice = None origin.lines = lines self.origins = origins @fields.depends('lines', 'journal', 'company') def on_change_lines(self): pool = Pool() Line = pool.get('account.statement.line') if not self.journal or not self.lines or not self.company: return if self.journal.currency != self.company.currency: return invoices = set() for line in self.lines: if (line.invoice and line.invoice.currency == self.company.currency): invoices.add(line.invoice) invoice_id2amount_to_pay = {} for invoice in invoices: if invoice.type == 'out': sign = -1 else: sign = 1 invoice_id2amount_to_pay[invoice.id] = sign * invoice.amount_to_pay lines = list(self.lines) line_offset = 0 for index, line in enumerate(self.lines or []): if line.invoice and line.id: if line.invoice.id not in invoice_id2amount_to_pay: continue amount_to_pay = invoice_id2amount_to_pay[line.invoice.id] if (amount_to_pay and getattr(line, 'amount', None) and (line.amount >= 0) == (amount_to_pay <= 0)): if abs(line.amount) > abs(amount_to_pay): new_line = Line() for field_name, field in Line._fields.items(): if field_name == 'id': continue try: setattr(new_line, field_name, getattr(line, field_name)) except AttributeError: pass new_line.amount = line.amount + amount_to_pay new_line.invoice = None line_offset += 1 lines.insert(index + line_offset, new_line) invoice_id2amount_to_pay[line.invoice.id] = 0 line.amount = amount_to_pay.copy_sign(line.amount) else: invoice_id2amount_to_pay[line.invoice.id] = ( line.amount + amount_to_pay) else: line.invoice = None self.lines = lines @fields.depends('journal') def on_change_with_validation(self, name=None): if self.journal: return self.journal.validation def get_to_reconcile(self, name=None): return bool(self.lines_to_reconcile) @property def lines_to_reconcile(self): lines = [] for line in self.lines: if line.move: for move_line in line.move.lines: if (move_line.account.reconcile and not move_line.reconciliation): lines.append(move_line) return lines def _group_key(self, line): key = ( ('number', line.number or Unequal()), ('date', line.date), ('party', line.party), ) return key def _get_grouped_line(self): "Return Line class for grouped lines" lines = self.origins or self.lines assert lines keys = [k[0] for k in self._group_key(lines[0])] class Line(namedtuple('Line', keys + ['lines'])): @property def amount(self): return sum((l.amount for l in self.lines)) @property def descriptions(self): done = set() for line in self.lines: if line.description and line.description not in done: done.add(line.description) yield line.description return Line @property def grouped_lines(self): if self.origins: lines = self.origins elif self.lines: lines = self.lines else: return Line = self._get_grouped_line() for key, lines in groupby(lines, key=self._group_key): yield Line(**dict(key + (('lines', list(lines)),))) @classmethod def view_attributes(cls): return super().view_attributes() + [ ('/tree', 'visual', If(Eval('state') == 'cancelled', 'muted', '')), ] @classmethod def delete(cls, statements): # Cancel before delete cls.cancel(statements) for statement in statements: if statement.state != 'cancelled': raise AccessError( gettext('account_statement.msg_statement_delete_cancel', statement=statement.rec_name)) super(Statement, cls).delete(statements) @classmethod @ModelView.button @Workflow.transition('draft') def draft(cls, statements): pass def validate_balance(self): pool = Pool() Lang = pool.get('ir.lang') amount = (self.start_balance + sum(l.amount for l in self.lines)) if amount != self.end_balance: lang = Lang.get() end_balance = lang.currency( self.end_balance, self.journal.currency) amount = lang.currency(amount, self.journal.currency) raise StatementValidateError( gettext('account_statement.msg_statement_wrong_end_balance', statement=self.rec_name, end_balance=end_balance, amount=amount)) def validate_amount(self): pool = Pool() Lang = pool.get('ir.lang') amount = sum(l.amount for l in self.lines) if amount != self.total_amount: lang = Lang.get() total_amount = lang.currency( self.total_amount, self.journal.currency) amount = lang.currency(amount, self.journal.currency) raise StatementValidateError( gettext('account_statement.msg_statement_wrong_total_amount', statement=self.rec_name, total_amount=total_amount, amount=amount)) def validate_number_of_lines(self): number = len(list(self.grouped_lines)) if number > self.number_of_lines: raise StatementValidateError( gettext('account_statement' '.msg_statement_wrong_number_of_lines_remove', statement=self.rec_name, n=number - self.number_of_lines)) elif number < self.number_of_lines: raise StatementValidateError( gettext('account_statement' '.msg_statement_wrong_number_of_lines_remove', statement=self.rec_name, n=self.number_of_lines - number)) @classmethod @ModelView.button @Workflow.transition('validated') def validate_statement(cls, statements): pool = Pool() Line = pool.get('account.statement.line') Warning = pool.get('res.user.warning') paid_cancelled_invoice_lines = [] for statement in statements: getattr(statement, 'validate_%s' % statement.validation)() paid_cancelled_invoice_lines.extend(l for l in statement.lines if l.invoice and l.invoice.state in {'cancelled', 'paid'}) if paid_cancelled_invoice_lines: warning_key = Warning.format( 'statement_paid_cancelled_invoice_lines', paid_cancelled_invoice_lines) if Warning.check(warning_key): raise StatementValidateWarning(warning_key, gettext('account_statement' '.msg_statement_invoice_paid_cancelled')) Line.write(paid_cancelled_invoice_lines, { 'related_to': None, }) cls.create_move(statements) cls.write(statements, { 'state': 'validated', }) common_lines = [l for l in Line.search([ ('statement.state', '=', 'draft'), ('related_to.state', 'in', ['posted', 'paid'], 'account.invoice'), ]) if l.invoice.reconciled] if common_lines: warning_key = '_'.join(str(l.id) for l in common_lines) if Warning.check(warning_key): raise StatementValidateWarning(warning_key, gettext('account_statement' '.msg_statement_paid_invoice_draft')) Line.write(common_lines, { 'related_to': None, }) @classmethod def create_move(cls, statements): '''Create move for the statements and try to reconcile the lines. Returns the list of move, statement and lines ''' pool = Pool() Line = pool.get('account.statement.line') Move = pool.get('account.move') MoveLine = pool.get('account.move.line') moves = [] for statement in statements: for key, lines in groupby( statement.lines, key=statement._group_key): lines = list(lines) key = dict(key) move = statement._get_move(key) moves.append((move, statement, lines)) Move.save([m for m, _, _ in moves]) to_write = [] for move, _, lines in moves: to_write.append(lines) to_write.append({ 'move': move.id, }) if to_write: Line.write(*to_write) move_lines = [] for move, statement, lines in moves: amount = 0 amount_second_currency = 0 for line in lines: move_line = line.get_move_line() move_line.move = move amount += move_line.debit - move_line.credit if move_line.amount_second_currency: amount_second_currency += move_line.amount_second_currency move_lines.append((move_line, line)) move_line = statement._get_move_line( amount, amount_second_currency, lines) move_line.move = move move_lines.append((move_line, None)) MoveLine.save([l for l, _ in move_lines]) Line.reconcile(move_lines) return moves def _get_move(self, key): 'Return Move for the grouping key' pool = Pool() Move = pool.get('account.move') Period = pool.get('account.period') period_id = Period.find(self.company.id, date=key['date']) return Move( period=period_id, journal=self.journal.journal, date=key['date'], origin=self, company=self.company, description=str(key['number']), ) def _get_move_line(self, amount, amount_second_currency, lines): 'Return counterpart Move Line for the amount' pool = Pool() MoveLine = pool.get('account.move.line') if self.journal.currency != self.company.currency: second_currency = self.journal.currency amount_second_currency *= -1 else: second_currency = None amount_second_currency = None descriptions = {l.description for l in lines} if len(descriptions) == 1: description, = descriptions else: description = '' return MoveLine( debit=abs(amount) if amount < 0 else 0, credit=abs(amount) if amount > 0 else 0, account=self.journal.account, second_currency=second_currency, amount_second_currency=amount_second_currency, description=description, ) @classmethod @ModelView.button @Workflow.transition('posted') def post(cls, statements): pool = Pool() Lang = pool.get('ir.lang') StatementLine = pool.get('account.statement.line') for statement in statements: for origin in statement.origins: if origin.pending_amount: lang = Lang.get() amount = lang.currency( origin.pending_amount, statement.journal.currency) raise StatementPostError( gettext('account_statement' '.msg_statement_post_pending_amount', statement=statement.rec_name, amount=amount, origin=origin.rec_name)) # Write state to skip statement test on Move.post cls.write(statements, {'state': 'posted'}) lines = [l for s in statements for l in s.lines] StatementLine.post_move(lines) @classmethod @ModelView.button @Workflow.transition('cancelled') def cancel(cls, statements): StatementLine = Pool().get('account.statement.line') lines = [l for s in statements for l in s.lines] StatementLine.delete_move(lines) @classmethod @ModelView.button_action('account_statement.act_reconcile') def reconcile(cls, statements): pass @classmethod def copy(cls, statements, default=None): default = default.copy() if default is not None else {} new_statements = [] for origins, sub_statements in groupby( statements, key=lambda s: bool(s.origins)): sub_statements = list(sub_statements) sub_default = default.copy() if origins: sub_default.setdefault('lines') new_statements.extend(super().copy( statements, default=sub_default)) return new_statements
class ActionActWindow(ActionMixin, ModelSQL, ModelView): "Action act window" __name__ = 'ir.action.act_window' domain = fields.Char('Domain Value') context = fields.Char('Context Value') order = fields.Char('Order Value') res_model = fields.Char('Model') context_model = fields.Char('Context Model') context_domain = fields.Char( "Context Domain", help="Part of the domain that will be evaluated on each refresh.") act_window_views = fields.One2Many('ir.action.act_window.view', 'act_window', 'Views') views = fields.Function(fields.Binary('Views'), 'get_views') act_window_domains = fields.One2Many('ir.action.act_window.domain', 'act_window', 'Domains') domains = fields.Function(fields.Binary('Domains'), 'get_domains') limit = fields.Integer('Limit', help='Default limit for the list view.') action = fields.Many2One('ir.action', 'Action', required=True, ondelete='CASCADE') search_value = fields.Char( 'Search Criteria', help='Default search criteria for the list view.') pyson_domain = fields.Function(fields.Char('PySON Domain'), 'get_pyson') pyson_context = fields.Function(fields.Char('PySON Context'), 'get_pyson') pyson_order = fields.Function(fields.Char('PySON Order'), 'get_pyson') pyson_search_value = fields.Function(fields.Char('PySON Search Criteria'), 'get_pyson') @classmethod def __setup__(cls): super(ActionActWindow, cls).__setup__() cls.__rpc__.update({ 'get': RPC(cache=dict(days=1)), }) @classmethod def __register__(cls, module_name): cursor = Transaction().connection.cursor() act_window = cls.__table__() super(ActionActWindow, cls).__register__(module_name) table = cls.__table_handler__(module_name) # Migration from 3.0: auto_refresh removed table.drop_column('auto_refresh') # Migration from 4.0: window_name removed table.drop_column('window_name') # Migration from 4.2: remove required on limit table.not_null_action('limit', 'remove') cursor.execute(*act_window.update([act_window.limit], [Null], where=act_window.limit == 0)) @staticmethod def default_type(): return 'ir.action.act_window' @staticmethod def default_context(): return '{}' @staticmethod def default_search_value(): return '[]' @classmethod def validate(cls, actions): super(ActionActWindow, cls).validate(actions) cls.check_views(actions) cls.check_domain(actions) cls.check_context(actions) @classmethod def check_views(cls, actions): "Check views" for action in actions: if action.res_model: for act_window_view in action.act_window_views: view = act_window_view.view if view.model != action.res_model: raise ViewError( gettext('ir.msg_action_invalid_views', view=view.rec_name, action=action.rec_name)) if view.type == 'board': raise ViewError( gettext('ir.msg_action_invalid_views', view=view.rec_name, action=action.rec_name)) else: for act_window_view in action.act_window_views: view = act_window_view.view if view.model: raise ViewError( gettext('ir.msg_action_invalid_views', view=view.rec_name, action=action.rec_name)) if view.type != 'board': raise ViewError( gettext('ir.msg_action_invalid_views', view=view.rec_name, action=action.rec_name)) @classmethod def check_domain(cls, actions): "Check domain and search_value" for action in actions: for domain in (action.domain, action.search_value): if not domain: continue try: value = PYSONDecoder().decode(domain) except Exception: raise DomainError( gettext('ir.msg_action_invalid_domain', domain=domain, action=action.rec_name)) if isinstance(value, PYSON): if not value.types() == set([list]): raise DomainError( gettext('ir.msg_action_invalid_domain', domain=domain, action=action.rec_name)) elif not isinstance(value, list): raise DomainError( gettext('ir.msg_action_invalid_domain', domain=domain, action=action.rec_name)) else: try: fields.domain_validate(value) except Exception: raise DomainError( gettext('ir.msg_action_invalid_domain', domain=domain, action=action.rec_name)) @classmethod def check_context(cls, actions): "Check context" for action in actions: if action.context: try: value = PYSONDecoder().decode(action.context) except Exception: raise ContextError( gettext('ir.msg_action_invalid_context', context=action.context, action=action.rec_name)) if isinstance(value, PYSON): if not value.types() == set([dict]): raise ContextError( gettext('ir.msg_action_invalid_context', context=action.context, action=action.rec_name)) elif not isinstance(value, dict): raise ContextError( gettext('ir.msg_action_invalid_context', context=action.context, action=action.rec_name)) else: try: fields.context_validate(value) except Exception: raise ContextError( gettext('ir.msg_action_invalid_context', context=action.context, action=action.rec_name)) def get_views(self, name): return [(view.view.id, view.view.type) for view in self.act_window_views] def get_domains(self, name): return [(domain.name, domain.domain or '[]', domain.count) for domain in self.act_window_domains] @classmethod def get_pyson(cls, windows, name): pool = Pool() encoder = PYSONEncoder() pysons = {} field = name[6:] defaults = { 'domain': '[]', 'context': '{}', 'search_value': '[]', } for window in windows: if not window.order and field == 'order': if window.res_model: defaults['order'] = encoder.encode( getattr(pool.get(window.res_model), '_order', 'null')) else: defaults['order'] = 'null' pysons[window.id] = (getattr(window, field) or defaults.get(field, 'null')) return pysons @classmethod def get(cls, xml_id): 'Get values from XML id or id' pool = Pool() ModelData = pool.get('ir.model.data') Action = pool.get('ir.action') if '.' in xml_id: action_id = ModelData.get_id(*xml_id.split('.')) else: action_id = int(xml_id) return Action(action_id).get_action_value()
class CreateSampleStart(metaclass=PoolMeta): __name__ = 'lims.create_sample.start' ind_required = fields.Function(fields.Boolean('Industry required'), 'on_change_with_ind_required') equipment = fields.Many2One('lims.equipment', 'Equipment', domain=[('party', '=', Eval('party'))], states={'required': Bool(Eval('ind_required'))}, context={'party': Eval('party')}, depends=['party', 'ind_required']) component = fields.Many2One('lims.component', 'Component', domain=[('equipment', '=', Eval('equipment'))], states={'required': Bool(Eval('ind_required'))}, depends=['equipment', 'ind_required']) comercial_product = fields.Many2One('lims.comercial.product', 'Comercial Product', depends=['ind_required'], states={'required': Bool(Eval('ind_required'))}) label = fields.Char('Label') ind_sampling_date = fields.Date('Sampling date') ind_volume = fields.Float('Received volume', depends=['ind_required'], states={'required': Bool(Eval('ind_required'))}) sampling_type = fields.Many2One('lims.sampling.type', 'Sampling Type', depends=['ind_required'], states={'required': Bool(Eval('ind_required'))}) ind_operational_detail = fields.Text('Operational detail') ind_work_environment = fields.Text('Work environment') ind_analysis_reason = fields.Text('Reason for analysis') missing_data = fields.Boolean('Missing data') attributes_domain = fields.Function(fields.Many2Many( 'lims.sample.attribute', None, None, 'Attributes domain'), 'on_change_with_attributes_domain') sample_photo = fields.Binary('Sample Photo') label_photo = fields.Binary('Label Photo') oil_added = fields.Float('Liters Oil added') ind_equipment = fields.Integer('Equipment') ind_equipment_uom = fields.Selection([ ('hs', 'Hs.'), ('km', 'Km.'), ], 'UoM', sort=False) ind_component = fields.Integer('Component') ind_component_uom = fields.Selection([ ('hs', 'Hs.'), ('km', 'Km.'), ], 'UoM', sort=False) ind_oil = fields.Integer('Oil') ind_oil_uom = fields.Selection([ ('hs', 'Hs.'), ('km', 'Km.'), ], 'UoM', sort=False) oil_changed = fields.Selection([ (None, '-'), ('yes', 'Yes'), ('no', 'No'), ], 'Did change Oil?', sort=False) oil_filter_changed = fields.Selection([ (None, '-'), ('yes', 'Yes'), ('no', 'No'), ], 'Did change Oil Filter?', sort=False) air_filter_changed = fields.Selection([ (None, '-'), ('yes', 'Yes'), ('no', 'No'), ], 'Did change Air Filter?', sort=False) @classmethod def __setup__(cls): super().__setup__() for field in ('component', 'comercial_product'): cls.analysis_domain.on_change_with.add(field) cls.product_type.states['readonly'] = Bool(Eval('component')) if 'component' not in cls.product_type.depends: cls.product_type.depends.append('component') cls.matrix.states['readonly'] = Bool(Eval('comercial_product')) if 'comercial_product' not in cls.matrix.depends: cls.matrix.depends.append('comercial_product') cls.attributes.domain = [('id', 'in', Eval('attributes_domain'))] if 'attributes_domain' not in cls.attributes.depends: cls.attributes.depends.append('attributes_domain') cls.sample_client_description.required = False @staticmethod def default_ind_equipment_uom(): return 'hs' @staticmethod def default_ind_component_uom(): return 'hs' @staticmethod def default_ind_oil_uom(): return 'hs' @fields.depends('fraction_type') def on_change_with_ind_required(self, name=None): Config = Pool().get('lims.configuration') if self.fraction_type: if self.fraction_type == Config(1).mcl_fraction_type: return True return False @fields.depends('equipment', 'component') def on_change_equipment(self): if not self.equipment and self.component: self.component = None @fields.depends('component') def on_change_component(self): if self.component: if self.component.product_type: self.product_type = self.component.product_type.id if self.component.comercial_product: self.comercial_product = self.component.comercial_product.id self.on_change_comercial_product() @fields.depends('comercial_product') def on_change_comercial_product(self): if self.comercial_product and self.comercial_product.matrix: self.matrix = self.comercial_product.matrix.id @fields.depends('product_type', 'component', '_parent_product_type.attribute_set') def on_change_with_attributes_domain(self, name=None): pool = Pool() SampleAttributeAttributeSet = pool.get( 'lims.sample.attribute-attribute.set') attribute_set = None if self.product_type and self.product_type.attribute_set: attribute_set = self.product_type.attribute_set.id res = SampleAttributeAttributeSet.search([ ('attribute_set', '=', attribute_set), ]) return [x.attribute.id for x in res] @fields.depends('label') def on_change_label(self): self.labels = self.label @fields.depends('packages_quantity', 'package_type') def on_change_with_ind_volume(self, name=None): if (self.packages_quantity and self.package_type and self.package_type.capacity): return (self.packages_quantity * self.package_type.capacity) return None @fields.depends('fraction_type', 'sampling_type') def on_change_fraction_type(self): super().on_change_fraction_type() if not self.fraction_type: return if (not self.sampling_type and self.fraction_type.default_sampling_type): self.sampling_type = self.fraction_type.default_sampling_type
class Newborn(ModelSQL, ModelView): 'Newborn Information' __name__ = 'gnuhealth.newborn' name = fields.Char('Newborn ID') patient = fields.Many2One('gnuhealth.patient', 'Baby', required=True, help="Patient associated to this newborn") mother = fields.Many2One('gnuhealth.patient', 'Mother') newborn_name = fields.Char('Name at Birth') birth_date = fields.DateTime('DoB', required=True, help="Date and Time of birth") photo = fields.Binary('Picture') newborn_sex = fields.Function( fields.Selection([ ('m', 'Male'), ('f', 'Female'), ], 'Sex'), 'get_newborn_sex') # Sex / Gender at birth. sex = fields.Selection([ ('m', 'Male'), ('f', 'Female'), ], 'Sex',sort=False, required=True, help="Sex at birth. It might differ from the current patient" \ " sex") cephalic_perimeter = fields.Integer( 'CP', help="Cephalic Perimeter in centimeters (cm)") length = fields.Integer('Length', help="Length in centimeters (cm)") weight = fields.Integer('Weight', help="Weight in grams (g)") apgar1 = fields.Integer('APGAR 1st minute') apgar5 = fields.Integer('APGAR 5th minute') apgar_scores = fields.One2Many('gnuhealth.neonatal.apgar', 'name', 'APGAR scores') meconium = fields.Boolean('Meconium') congenital_diseases = fields.One2Many('gnuhealth.patient.disease', 'newborn_id', 'Congenital diseases') reanimation_stimulation = fields.Boolean('Stimulation') reanimation_aspiration = fields.Boolean('Aspiration') reanimation_intubation = fields.Boolean('Intubation') reanimation_mask = fields.Boolean('Mask') reanimation_oxygen = fields.Boolean('Oxygen') test_vdrl = fields.Boolean('VDRL') test_toxo = fields.Boolean('Toxoplasmosis') test_chagas = fields.Boolean('Chagas') test_billirubin = fields.Boolean('Billirubin') test_audition = fields.Boolean('Audition') test_metabolic = fields.Boolean( 'Metabolic ("heel stick screening")', help="Test for Fenilketonuria, Congenital Hypothyroidism, " "Quistic Fibrosis, Galactosemia") neonatal_ortolani = fields.Boolean('Positive Ortolani') neonatal_barlow = fields.Boolean('Positive Barlow') neonatal_hernia = fields.Boolean('Hernia') neonatal_ambiguous_genitalia = fields.Boolean('Ambiguous Genitalia') neonatal_erbs_palsy = fields.Boolean('Erbs Palsy') neonatal_hematoma = fields.Boolean('Hematomas') neonatal_talipes_equinovarus = fields.Boolean('Talipes Equinovarus') neonatal_polydactyly = fields.Boolean('Polydactyly') neonatal_syndactyly = fields.Boolean('Syndactyly') neonatal_moro_reflex = fields.Boolean('Moro Reflex') neonatal_grasp_reflex = fields.Boolean('Grasp Reflex') neonatal_stepping_reflex = fields.Boolean('Stepping Reflex') neonatal_babinski_reflex = fields.Boolean('Babinski Reflex') neonatal_blink_reflex = fields.Boolean('Blink Reflex') neonatal_sucking_reflex = fields.Boolean('Sucking Reflex') neonatal_swimming_reflex = fields.Boolean('Swimming Reflex') neonatal_tonic_neck_reflex = fields.Boolean('Tonic Neck Reflex') neonatal_rooting_reflex = fields.Boolean('Rooting Reflex') neonatal_palmar_crease = fields.Boolean('Transversal Palmar Crease') medication = fields.One2Many('gnuhealth.patient.medication', 'newborn_id', 'Medication') responsible = fields.Many2One('gnuhealth.healthprofessional', 'Doctor in charge', help="Signed by the health professional") dismissed = fields.DateTime('Discharged') bd = fields.Boolean('Stillbirth') died_at_delivery = fields.Boolean('Died at delivery room') died_at_the_hospital = fields.Boolean('Died at the hospital') died_being_transferred = fields.Boolean( 'Died being transferred', help="The baby died being transferred to another health institution") tod = fields.DateTime('Time of Death') cod = fields.Many2One('gnuhealth.pathology', 'Cause of death') notes = fields.Text('Notes') @classmethod def __setup__(cls): super(Newborn, cls).__setup__() cls._sql_constraints = [ ('name_uniq', 'unique(name)', 'The Newborn ID must be unique !'), ] def get_newborn_sex(self, name): if self.patient: return self.patient.sex
class ImportDataBinary(ModelSQL): "Import Data Binary" __name__ = 'test.import_data.binary' data = fields.Binary("Data")
class Attachment: __metaclass__ = PoolMeta __name__ = 'ir.attachment' cryptolog_signer = fields.Many2One('party.party', 'Cryptolog Signer', ondelete='RESTRICT', states={ 'readonly': Bool(Eval('cryptolog_status')) }, depends=['cryptolog_status'] ) cryptolog_id = fields.Char('Cryptolog ID', readonly=True) cryptolog_url = fields.Char('Cryptolog URL', readonly=True) cryptolog_status = fields.Selection([ ('', ''), ('issued', 'Issued'), ('ready', 'Ready'), ('expired', 'Expired'), ('canceled', 'Canceled'), ('failed', 'Failed'), ('completed', 'Completed'), ], 'Cryptolog Status', readonly=True) cryptolog_data = fields.Function( fields.Binary('Signed Document', filename='name', states={ 'invisible': Eval('cryptolog_status') != 'completed' }, depends=['cryptolog_status']), 'cryptolog_get_documents') cryptolog_logs = fields.Text('Cryptolog Logs', readonly=True) @classmethod def __setup__(cls): super(Attachment, cls).__setup__() cls._buttons.update({ 'cryptolog_request_transaction': {}, 'cryptolog_get_transaction_info': {} }) @classmethod def cryptolog_headers(cls): return {'Content-Type': 'application/xml'} @classmethod def cryptolog_basic_auth(cls): assert (config.get(CONFIG_SECTION, 'auth_mode') == 'basic') username = config.get(CONFIG_SECTION, 'username') assert username password = config.get(CONFIG_SECTION, 'password') assert password return requests.auth.HTTPBasicAuth(username, password) def append_log(self, method, response): self.cryptolog_logs = self.cryptolog_logs or '' self.cryptolog_logs += '%s @ %s\n%s\n\n' % (method, datetime.datetime.utcnow(), response) @classmethod @ModelView.button def cryptolog_request_transaction(cls, attachments): # for now we support only one record attachment, = attachments url = config.get(CONFIG_SECTION, 'url') assert url verify = True if config.get(CONFIG_SECTION, 'no_verify') == '1': verify = False method = 'requester.requestTransaction' headers = cls.cryptolog_headers() auth = cls.cryptolog_basic_auth() data = { 'documents': [{ 'documentType': 'pdf', 'name': attachment.name, 'content': xmlrpclib.Binary(attachment.data) }], 'signers': [{ 'lastname': attachment.cryptolog_signer.full_name, 'emailAddress': attachment.cryptolog_signer.email, 'phoneNum': attachment.cryptolog_signer.phone }], 'mustContactFirstSigner': True, 'finalDocSent': True } successURL = config.get(CONFIG_SECTION, 'success-url') if successURL is not None: successURL = successURL.format(att=attachment) data['signers'][0]['successURL'] = successURL failURL = config.get(CONFIG_SECTION, 'fail-url') if failURL is not None: failURL = failURL.format(att=attachment) data['signers'][0]['failURL'] = failURL cancelURL = config.get(CONFIG_SECTION, 'cancel-url') if cancelURL is not None: cancelURL = cancelURL.format(att=attachment) data['signers'][0]['cancelURL'] = cancelURL data = xmlrpclib.dumps((data,), method) req = requests.post(url, headers=headers, auth=auth, data=data, verify=verify) if req.status_code > 299: raise Exception(req.content) response, _ = xmlrpclib.loads(req.content) attachment.cryptolog_status = 'issued' attachment.append_log(method, response) attachment.cryptolog_id = response[0]['id'] attachment.cryptolog_url = response[0]['url'] attachment.save() @classmethod @ModelView.button def cryptolog_get_transaction_info(cls, attachments): attachment, = attachments url = config.get(CONFIG_SECTION, 'url') assert url verify = True if config.get(CONFIG_SECTION, 'no_verify') == '1': verify = False method = 'requester.getTransactionInfo' headers = cls.cryptolog_headers() auth = cls.cryptolog_basic_auth() data = xmlrpclib.dumps((attachment.cryptolog_id,), method) req = requests.post(url, headers=headers, auth=auth, data=data, verify=verify) response, _ = xmlrpclib.loads(req.content) attachment.append_log(method, response) attachment.cryptolog_status = response[0]['status'] attachment.save() def cryptolog_get_documents(self, name): # tryton trick (extra param on context to retrieve file size) if self.cryptolog_id and self.cryptolog_status == 'completed': if Transaction().context.get('%s.%s' % (self.__name__, name)) == \ 'size': # does not make sense to retrieve the doc juste for the size return 1024 url = config.get(CONFIG_SECTION, 'url') verify = True if config.get(CONFIG_SECTION, 'no_verify') == '1': verify = False method = 'requester.getDocuments' headers = self.cryptolog_headers() auth = self.cryptolog_basic_auth() data = xmlrpclib.dumps((self.cryptolog_id,), method) req = requests.post(url, headers=headers, auth=auth, data=data, verify=verify) response, _ = xmlrpclib.loads(req.content) return response[0][0]['content']