class AssetRevision(ModelSQL, ModelView): "Asset Revision" __name__ = 'account.asset.revision' currency = fields.Function( fields.Many2One('currency.currency', "Currency"), 'on_change_with_currency') value = Monetary("Asset Value", currency='currency', digits='currency', required=True) residual_value = Monetary("Residual Value", currency='currency', digits='currency', required=True) end_date = fields.Date("End Date", required=True) origin = fields.Reference("Origin", selection='get_origins', select=True) description = fields.Char("Description") asset = fields.Many2One('account.asset', "Asset", select=True, required=True) @classmethod def __setup__(cls): super().__setup__() cls.__access__.add('asset') @fields.depends('asset', '_parent_asset.currency') def on_change_with_currency(self, name=None): if self.asset and self.asset.currency: return self.asset.currency.id @fields.depends('origin', 'value', 'asset', '_parent_asset.value') def on_change_origin(self, name=None): pool = Pool() InvoiceLine = pool.get('account.invoice.line') if isinstance(self.origin, InvoiceLine) and self.origin.id >= 0: self.value = self.asset.value + self.origin.amount @staticmethod def _get_origin(): "Return list of Model names for origin Reference" return ['account.invoice.line'] @classmethod def get_origins(cls): pool = Pool() IrModel = pool.get('ir.model') get_name = IrModel.get_name models = cls._get_origin() return [(None, '')] + [(m, get_name(m)) for m in models]
class TestPaymentTermView(ModelView): 'Test Payment Term' __name__ = 'account.invoice.payment_term.test' payment_term = fields.Many2One('account.invoice.payment_term', 'Payment Term', required=True) date = fields.Date('Date') amount = Monetary( "Amount", currency='currency', digits='currency', required=True) currency = fields.Many2One('currency.currency', 'Currency', required=True) result = fields.One2Many('account.invoice.payment_term.test.result', None, 'Result', readonly=True) @staticmethod def default_currency(): pool = Pool() Company = pool.get('company.company') company = Transaction().context.get('company') if company: return Company(company).currency.id @fields.depends('payment_term', 'date', 'amount', 'currency', 'result') def on_change_with_result(self): pool = Pool() Result = pool.get('account.invoice.payment_term.test.result') result = [] if (self.payment_term and self.amount and self.currency): for date, amount in self.payment_term.compute( self.amount, self.currency, self.date): result.append(Result( date=date, amount=amount, currency=self.currency)) self.result = result return self._changed_values.get('result', [])
class TestPaymentTermViewResult(ModelView): 'Test Payment Term' __name__ = 'account.invoice.payment_term.test.result' date = fields.Date('Date', readonly=True) amount = Monetary( "Amount", currency='currency', digits='currency', readonly=True) currency = fields.Many2One('currency.currency', "Currency")
class WeightPriceList(ModelSQL, ModelView): 'Carrier Weight Price List' __name__ = 'carrier.weight_price_list' carrier = fields.Many2One( 'carrier', 'Carrier', required=True, select=True, help="The carrier that the price list belongs to.") weight = fields.Float('Weight', digits=(16, Eval('_parent_carrier', {}).get('weight_uom_digits', 2)), depends={'carrier'}, help="The lower limit for the price.") price = Monetary("Price", currency='currency', digits='currency', help="The price of the carrier service.") currency = fields.Function( fields.Many2One('currency.currency', "Currency"), 'on_change_with_currency') @classmethod def __setup__(cls): super(WeightPriceList, cls).__setup__() cls._order.insert(0, ('weight', 'ASC')) @fields.depends('carrier', '_parent_carrier.weight_currency') def on_change_with_currency(self, name=None): if self.carrier and self.carrier.weight_currency: return self.carrier.weight_currency.id
class AssetLine(ModelSQL, ModelView): 'Asset Line' __name__ = 'account.asset.line' asset = fields.Many2One('account.asset', 'Asset', required=True, ondelete='CASCADE', readonly=True) date = fields.Date('Date', readonly=True) depreciation = Monetary("Depreciation", currency='currency', digits='currency', required=True, readonly=True) acquired_value = Monetary("Acquired Value", currency='currency', digits='currency', readonly=True) depreciable_basis = Monetary("Depreciable Basis", currency='currency', digits='currency', readonly=True) actual_value = Monetary("Actual Value", currency='currency', digits='currency', readonly=True) accumulated_depreciation = Monetary("Accumulated Depreciation", currency='currency', digits='currency', readonly=True) move = fields.Many2One('account.move', 'Account Move', readonly=True) currency = fields.Function( fields.Many2One('currency.currency', 'Currency'), 'on_change_with_currency') @classmethod def __setup__(cls): super(AssetLine, cls).__setup__() cls.__access__.add('asset') cls._order.insert(0, ('date', 'ASC')) @fields.depends('asset', '_parent_asset.currency') def on_change_with_currency(self, name=None): if self.asset: return self.asset.currency.id
class PurchaseRequisitionLine(sequence_ordered(), ModelSQL, ModelView): "Purchase Requisition Line" __name__ = 'purchase.requisition.line' _states = { 'readonly': Eval('purchase_requisition_state') != 'draft', } requisition = fields.Many2One( 'purchase.requisition', 'Requisition', ondelete='CASCADE', select=True, required=True) supplier = fields.Many2One('party.party', 'Supplier', states=_states) product = fields.Many2One( 'product.product', 'Product', ondelete='RESTRICT', domain=[ ('purchasable', '=', True), ], states=_states) product_uom_category = fields.Function( fields.Many2One('product.uom.category', "Product UOM Category"), 'on_change_with_product_uom_category') description = fields.Text("Description", states=_states) summary = fields.Function(fields.Char('Summary'), 'on_change_with_summary') quantity = fields.Float( "Quantity", digits='unit', required=True, states=_states) unit = fields.Many2One( 'product.uom', 'Unit', ondelete='RESTRICT', states={ 'required': Bool(Eval('product')), 'readonly': _states['readonly'], }) unit_price = Monetary( 'Unit Price', currency='currency', digits=price_digits, states=_states) currency = fields.Function(fields.Many2One('currency.currency', 'Currency'), 'on_change_with_currency') amount = fields.Function(Monetary( "Amount", currency='currency', digits='currency'), 'on_change_with_amount') purchase_requests = fields.One2Many( 'purchase.request', 'origin', 'Purchase Request', readonly=True) purchase_requisition_state = fields.Function(fields.Selection( 'get_purchase_requisition_states', "Purchase Requisition State"), 'on_change_with_purchase_requisition_state') del _states @classmethod def __setup__(cls): super().__setup__() cls.__access__.add('requisition') unit_categories = cls._unit_categories() cls.unit.domain = [ If(Bool(Eval('product_uom_category')), ('category', 'in', [Eval(c) for c in unit_categories]), ('category', '!=', -1)), ] @classmethod def _unit_categories(cls): return ['product_uom_category'] @fields.depends('product') def on_change_with_product_uom_category(self, name=None): if self.product: return self.product.default_uom_category.id @fields.depends('requisition', '_parent_requisition.currency') def on_change_with_currency(self, name=None): if self.requisition and self.requisition.currency: return self.requisition.currency.id @classmethod def get_purchase_requisition_states(cls): pool = Pool() Requisition = pool.get('purchase.requisition') return Requisition.fields_get(['state'])['state']['selection'] @fields.depends('requisition', '_parent_requisition.state') def on_change_with_purchase_requisition_state(self, name=None): if self.requisition: return self.requisition.state @fields.depends('product', 'unit', 'quantity', 'supplier') def on_change_product(self): if not self.product: return category = self.product.purchase_uom.category if not self.unit or self.unit.category != category: self.unit = self.product.purchase_uom @fields.depends('description') def on_change_with_summary(self, name=None): return firstline(self.description or '') @fields.depends('quantity', 'unit_price', 'unit', 'requisition', '_parent_requisition.currency') def on_change_with_amount(self, name=None): if (self.unit_price is None) or (self.quantity is None): return None amount = Decimal(str(self.quantity)) * self.unit_price if self.requisition.currency: amount = self.requisition.currency.round(amount) return amount def get_rec_name(self, name): pool = Pool() Lang = pool.get('ir.lang') if self.product: lang = Lang.get() return (lang.format_number_symbol( self.quantity or 0, self.unit, digits=self.unit.digits) + ' %s @ %s' % ( self.product.rec_name, self.requisition.rec_name)) else: return self.requisition.rec_name def _get_purchase_request_product_supplier_pattern(self): pattern = { 'company': self.requisition.company.id, } if self.supplier: pattern['party'] = self.supplier.id return pattern @property def request_unit(self): unit = self.unit if (self.product and self.product.purchase_uom.category == self.unit.category): unit = self.product.purchase_uom return unit @property def request_quantity(self): pool = Pool() Uom = pool.get('product.uom') quantity = self.quantity request_unit = self.request_unit if (self.product and request_unit and request_unit.category == self.unit.category): quantity = Uom.compute_qty( self.unit, self.quantity, request_unit, round=True) return quantity @property def request_unit_price(self): return self.unit_price def compute_request(self): """ Return the value of the purchase request which will answer to the needed quantity at the given date. """ pool = Pool() Uom = pool.get('product.uom') Request = pool.get('purchase.request') if self.purchase_requests: return supply_date = self.requisition.supply_date supplier = None purchase_date = None if self.product: supplier, purchase_date = Request.find_best_supplier( self.product, supply_date, **self._get_purchase_request_product_supplier_pattern()) elif self.supplier: lead_time = self.supplier.get_multivalue( 'supplier_lead_time', company=self.requisition.company.id) if lead_time is not None: purchase_date = supply_date - lead_time uom = self.request_unit quantity = self.request_quantity if (self.product and self.product.purchase_uom.category == self.unit.category): uom = self.product.purchase_uom quantity = Uom.compute_qty(self.unit, self.quantity, uom, round=True) return Request( product=self.product, description=self.description, party=supplier or self.supplier, quantity=quantity, uom=uom, computed_quantity=self.quantity, computed_uom=self.unit, purchase_date=purchase_date, supply_date=supply_date, company=self.requisition.company, warehouse=self.requisition.warehouse, origin=self, ) @classmethod def copy(cls, lines, default=None): if default is None: default = {} else: default = default.copy() default.setdefault('purchase_requests') return super(PurchaseRequisitionLine, cls).copy(lines, default=default)
class PaymentTermLine(sequence_ordered(), ModelSQL, ModelView): 'Payment Term Line' __name__ = 'account.invoice.payment_term.line' payment = fields.Many2One('account.invoice.payment_term', 'Payment Term', required=True, ondelete="CASCADE") type = fields.Selection([ ('fixed', 'Fixed'), ('percent', 'Percentage on Remainder'), ('percent_on_total', 'Percentage on Total'), ('remainder', 'Remainder'), ], 'Type', required=True) ratio = fields.Numeric('Ratio', digits=(14, 10), states={ 'invisible': ~Eval('type').in_(['percent', 'percent_on_total']), 'required': Eval('type').in_(['percent', 'percent_on_total']), }) divisor = fields.Numeric('Divisor', digits=(10, 14), states={ 'invisible': ~Eval('type').in_(['percent', 'percent_on_total']), 'required': Eval('type').in_(['percent', 'percent_on_total']), }) amount = Monetary( "Amount", currency='currency', digits='currency', states={ 'invisible': Eval('type') != 'fixed', 'required': Eval('type') == 'fixed', }) currency = fields.Many2One('currency.currency', 'Currency', states={ 'invisible': Eval('type') != 'fixed', 'required': Eval('type') == 'fixed', }) relativedeltas = fields.One2Many( 'account.invoice.payment_term.line.delta', 'line', 'Deltas') @classmethod def __setup__(cls): super().__setup__() cls.__access__.add('payment') @classmethod def __register__(cls, module_name): sql_table = cls.__table__() super(PaymentTermLine, cls).__register__(module_name) cursor = Transaction().connection.cursor() table = cls.__table_handler__(module_name) # Migration from 3.8: rename percentage into ratio if table.column_exist('percentage'): cursor.execute(*sql_table.update( columns=[sql_table.ratio], values=[sql_table.percentage / 100])) table.drop_column('percentage') @staticmethod def default_type(): return 'remainder' @classmethod def default_relativedeltas(cls): if Transaction().user == 0: return [] return [{}] @fields.depends('type') def on_change_type(self): if self.type != 'fixed': self.amount = Decimal('0.0') self.currency = None if self.type not in ('percent', 'percent_on_total'): self.ratio = Decimal('0.0') self.divisor = Decimal('0.0') @fields.depends('ratio') def on_change_ratio(self): if not self.ratio: self.divisor = Decimal('0.0') else: self.divisor = self.round(1 / self.ratio, self.__class__.divisor.digits[1]) @fields.depends('divisor') def on_change_divisor(self): if not self.divisor: self.ratio = Decimal('0.0') else: self.ratio = self.round(1 / self.divisor, self.__class__.ratio.digits[1]) def get_date(self, date): for relativedelta_ in self.relativedeltas: date += relativedelta_.get() return date def get_value(self, remainder, amount, currency): Currency = Pool().get('currency.currency') if self.type == 'fixed': fixed = Currency.compute(self.currency, self.amount, currency) return fixed.copy_sign(amount) elif self.type == 'percent': return currency.round(remainder * self.ratio) elif self.type == 'percent_on_total': return currency.round(amount * self.ratio) elif self.type == 'remainder': return currency.round(remainder) return None @staticmethod def round(number, digits): quantize = Decimal(10) ** -Decimal(digits) return Decimal(number).quantize(quantize) @classmethod def validate_fields(cls, lines, field_names): super().validate_fields(lines, field_names) cls.check_ratio_and_divisor(lines, field_names) @classmethod def check_ratio_and_divisor(cls, lines, field_names=None): "Check consistency between ratio and divisor" if field_names and not (field_names & {'type', 'ratio', 'divisor'}): return for line in lines: if line.type not in ('percent', 'percent_on_total'): continue if line.ratio is None or line.divisor is None: raise PaymentTermValidationError( gettext('account_invoice' '.msg_payment_term_invalid_ratio_divisor', line=line.rec_name)) if (line.ratio != round( 1 / line.divisor, cls.ratio.digits[1]) and line.divisor != round( 1 / line.ratio, cls.divisor.digits[1])): raise PaymentTermValidationError( gettext('account_invoice' '.msg_payment_term_invalid_ratio_divisor', line=line.rec_name))
class _Action_Line: __slots__ = () _states = { 'readonly': ((Eval('complaint_state') != 'draft') | Bool(Eval('_parent_action.result', True))), } action = fields.Many2One('sale.complaint.action', 'Action', ondelete='CASCADE', select=True, required=True) quantity = fields.Float("Quantity", digits='unit', states=_states) unit = fields.Function(fields.Many2One('product.uom', "Unit"), 'on_change_with_unit') unit_price = Monetary("Unit Price", currency='currency', digits=price_digits, states=_states, help='Leave empty for the same price.') amount = fields.Function( Monetary("Amount", currency='currency', digits='currency'), 'on_change_with_amount') currency = fields.Function( fields.Many2One('currency.currency', "Currency"), 'on_change_with_currency') complaint_state = fields.Function( fields.Selection('get_complaint_states', "Complaint State"), 'on_change_with_complaint_state') complaint_origin_id = fields.Function( fields.Integer("Complaint Origin ID"), 'on_change_with_complaint_origin_id') @classmethod def __setup__(cls): super().__setup__() cls.__access__.add('action') def on_change_with_unit(self, name=None): raise NotImplementedError @fields.depends('currency', methods=['get_quantity', 'get_unit_price']) def on_change_with_amount(self, name=None): quantity = self.get_quantity() or 0 unit_price = self.get_unit_price() or Decimal(0) amount = Decimal(str(quantity)) * unit_price if self.currency: amount = self.currency.round(amount) return amount def get_quantity(self): raise NotImplementedError def get_unit_price(self): raise NotImplementedError @fields.depends('action', '_parent_action.currency') def on_change_with_currency(self, name=None): if self.action and self.action.currency: return self.action.currency.id @classmethod def get_complaint_states(cls): pool = Pool() Complaint = pool.get('sale.complaint') return Complaint.fields_get(['state'])['state']['selection'] @fields.depends('action', '_parent_action.complaint', '_parent_action._parent_complaint.state') def on_change_with_complaint_state(self, name=None): if self.action and self.action.complaint: return self.action.complaint.state @fields.depends('action', '_parent_action.complaint', '_parent_action._parent_complaint.origin_id') def on_change_with_complaint_origin_id(self, name=None): if self.action and self.action.complaint: return self.action.complaint.origin_id
class PurchaseRequisition(Workflow, ModelSQL, ModelView): "Purchase Requisition" __name__ = 'purchase.requisition' _rec_name = 'number' _states = { 'readonly': Eval('state') != 'draft', } company = fields.Many2One( 'company.company', "Company", required=True, select=True, states={ 'readonly': (Eval('state') != 'draft') | Eval('lines', [0]), }) number = fields.Char('Number', readonly=True, select=True) description = fields.Char('Description', states=_states) employee = fields.Many2One( 'company.employee', 'Employee', required=True, states=_states) supply_date = fields.Date( 'Supply Date', states={ 'required': ~Eval('state').in_(['draft', 'cancelled']), 'readonly': _states['readonly'], }) warehouse = fields.Many2One( 'stock.location', 'Warehouse', domain=[ ('type', '=', 'warehouse'), ], states=_states) currency = fields.Many2One( 'currency.currency', 'Currency', states={ 'readonly': (_states['readonly'] | (Eval('lines', [0]) & Eval('currency'))), }) total_amount = fields.Function( Monetary("Total", currency='currency', digits='currency'), 'get_amount') total_amount_cache = Monetary( "Total Cache", currency='currency', digits='currency') lines = fields.One2Many( 'purchase.requisition.line', 'requisition', 'Lines', states=_states) approved_by = employee_field( "Approved By", states=['approved', 'processing', 'done', 'cancelled']) rejected_by = employee_field( "Rejected By", states=['rejected', 'processing', 'done', 'cancelled']) state = fields.Selection([ ('draft', "Draft"), ('waiting', "Waiting"), ('rejected', "Rejected"), ('approved', "Approved"), ('processing', "Processing"), ('done', "Done"), ('cancelled', "Cancelled"), ], "State", readonly=True, required=True, sort=False) del _states @classmethod def __setup__(cls): super(PurchaseRequisition, cls).__setup__() cls._transitions |= set(( ('cancelled', 'draft'), ('rejected', 'draft'), ('draft', 'cancelled'), ('draft', 'waiting'), ('waiting', 'draft'), ('waiting', 'rejected'), ('waiting', 'approved'), ('approved', 'processing'), ('approved', 'draft'), ('processing', 'done'), ('done', 'processing'), )) cls._buttons.update({ 'cancel': { 'invisible': Eval('state') != 'draft', 'depends': ['state'], }, 'draft': { 'invisible': ~Eval('state').in_( ['cancelled', 'waiting', 'approved', 'rejected']), 'icon': If(Eval('state').in_(['cancelled', 'rejected']), 'tryton-undo', 'tryton-back'), 'depends': ['state'], }, 'wait': { 'pre_validate': [('supply_date', '!=', None)], 'invisible': ((Eval('state') != 'draft') | ~Eval('lines', [])), 'readonly': ~Eval('lines', []), 'depends': ['state'], }, 'approve': { 'invisible': Eval('state') != 'waiting', 'depends': ['state'], }, 'process': { 'invisible': ~Eval('state').in_( ['approved', 'processing']), 'icon': If(Eval('state') == 'approved', 'tryton-forward', 'tryton-refresh'), 'depends': ['state'], }, 'reject': { 'invisible': Eval('state') != 'waiting', 'depends': ['state'], }, }) # The states where amounts are cached cls._states_cached = ['approved', 'done', 'rejected', 'processing', 'cancelled'] @classmethod def __register__(cls, module_name): cursor = Transaction().connection.cursor() table = cls.__table__() super().__register__(module_name) # Migration from 5.6: rename state cancel to cancelled cursor.execute(*table.update( [table.state], ['cancelled'], where=table.state == 'cancel')) @classmethod def default_state(cls): return 'draft' @classmethod def default_company(cls): return Transaction().context.get('company') @classmethod def default_employee(cls): return Transaction().context.get('employee') @classmethod def default_warehouse(cls): Location = Pool().get('stock.location') return Location.get_default_warehouse() @classmethod def default_currency(cls): Company = Pool().get('company.company') company = Transaction().context.get('company') if company: return Company(company).currency.id @fields.depends('lines', 'currency') def on_change_with_total_amount(self): self.total_amount = Decimal('0.0') if self.lines: for line in self.lines: self.total_amount += getattr(line, 'amount', None) or 0 if self.currency: self.total_amount = self.currency.round(self.total_amount) return self.total_amount @classmethod def store_cache(cls, requisitions): for requisition in requisitions: requisition.total_amount_cache = requisition.total_amount cls.save(requisitions) @classmethod def get_amount(cls, requisitions, name): total_amount = {} # Sort cached first and re-instantiate to optimize cache management requisitions = sorted(requisitions, key=lambda r: r.state in cls._states_cached, reverse=True) requisitions = cls.browse(requisitions) for requisition in requisitions: if (requisition.state in cls._states_cached and requisition.total_amount_cache is not None): total_amount[requisition.id] = requisition.total_amount_cache else: total_amount[requisition.id] = ( requisition.on_change_with_total_amount()) return total_amount @classmethod def create_requests(cls, requisitions): pool = Pool() Request = pool.get('purchase.request') requests = [] for requisition in requisitions: for line in requisition.lines: request = line.compute_request() if request: requests.append(request) if requests: Request.save(requests) @classmethod def view_attributes(cls): return super().view_attributes() + [ ('/tree', 'visual', If(Eval('state') == 'cancelled', 'muted', '')), ] @classmethod def create(cls, vlist): pool = Pool() Config = pool.get('purchase.configuration') config = Config(1) vlist = [v.copy() for v in vlist] default_company = cls.default_company() for values in vlist: if values.get('number') is None: values['number'] = config.get_multivalue( 'purchase_requisition_sequence', company=values.get('company', default_company)).get() return super(PurchaseRequisition, cls).create(vlist) @classmethod def delete(cls, requisitions): # Cancel before delete cls.cancel(requisitions) for requisition in requisitions: if requisition.state != 'cancelled': raise AccessError( gettext('purchase_requisition.msg_delete_cancel', requisition=requisition.rec_name)) super(PurchaseRequisition, cls).delete(requisitions) def check_for_waiting(self): if not self.warehouse: for line in self.lines: if line.product and line.product.type in {'goods', 'assets'}: raise RequiredValidationError( gettext('purchase_requisition.msg_warehouse_required', requisition=self.rec_name)) @classmethod def copy(cls, requisitions, default=None): if default is None: default = {} else: default = default.copy() default.setdefault('number', None) default.setdefault('supply_date', None) default.setdefault('approved_by') default.setdefault('rejected_by') return super(PurchaseRequisition, cls).copy( requisitions, default=default) @classmethod @ModelView.button @Workflow.transition('cancelled') def cancel(cls, requisitions): cls.store_cache(requisitions) @classmethod @ModelView.button @Workflow.transition('draft') @reset_employee('approved_by', 'rejected_by') def draft(cls, requisitions): pass @classmethod @ModelView.button @Workflow.transition('waiting') def wait(cls, requisitions): for requisition in requisitions: requisition.check_for_waiting() @classmethod @ModelView.button @Workflow.transition('rejected') @set_employee('rejected_by') def reject(cls, requisitions): pass @classmethod @ModelView.button @Workflow.transition('approved') @set_employee('approved_by') def approve(cls, requisitions): pool = Pool() Configuration = pool.get('purchase.configuration') transaction = Transaction() context = transaction.context cls.store_cache(requisitions) config = Configuration(1) with transaction.set_context( queue_scheduled_at=config.purchase_process_after, queue_batch=context.get('queue_batch', True)): cls.__queue__.process(requisitions) @classmethod @Workflow.transition('processing') def proceed(cls, requisitions): pass @classmethod @Workflow.transition('done') def do(cls, requisitions): pass @classmethod @ModelView.button def process(cls, requisitions): done = [] process = [] requisitions = [r for r in requisitions if r.state in {'approved', 'processing', 'done'}] cls.create_requests(requisitions) for requisition in requisitions: if requisition.is_done(): if requisition.state != 'done': done.append(requisition) elif requisition.state != 'processing': process.append(requisition) if process: cls.proceed(process) if done: cls.do(done) def is_done(self): return all( r.purchase and r.purchase.state in {'cancelled', 'confirmed'} for l in self.lines for r in l.purchase_requests)
class Work(metaclass=PoolMeta): __name__ = 'project.work' product = fields.Many2One('product.product', 'Product', domain=[ ('type', '=', 'service'), ], context={ 'company': Eval('company', -1), }, depends={'company'}) list_price = Monetary("List Price", currency='currency', digits=price_digits) revenue = fields.Function( Monetary("Revenue", currency='currency', digits='currency'), 'get_total') cost = fields.Function( Monetary("Cost", currency='currency', digits='currency'), 'get_total') currency = fields.Function( fields.Many2One('currency.currency', 'Currency'), 'on_change_with_currency') @classmethod def __setup__(cls): pool = Pool() super(Work, cls).__setup__() try: pool.get('purchase.line') except KeyError: pass else: # Add purchase lines if purchase is activated cls.purchase_lines = fields.One2Many('purchase.line', 'work', 'Purchase Lines', domain=[ ('purchase.company', '=', Eval('company', -1)), ('type', '=', 'line'), ]) @classmethod def _get_cost(cls, works): pool = Pool() Line = pool.get('timesheet.line') Work = pool.get('timesheet.work') transaction = Transaction() cursor = transaction.connection.cursor() costs = dict.fromkeys([w.id for w in works], 0) table = cls.__table__() work = Work.__table__() line = Line.__table__() # Timesheet cost work_ids = [w.id for w in works] for sub_ids in grouped_slice(work_ids): red_sql = reduce_ids(table.id, sub_ids) cursor.execute( *table.join(work, condition=(Concat(cls.__name__ + ',', table.id) == work.origin)). join(line, condition=line.work == work.id).select( table.id, Sum(line.cost_price * line.duration), where=red_sql, group_by=[table.id])) for work_id, cost in cursor: # SQLite stores timedelta as float if not isinstance(cost, float): cost = cost.total_seconds() # Convert from seconds cost /= 60 * 60 costs[work_id] += Decimal(str(cost)) # Purchase cost if hasattr(cls, 'purchase_lines'): for work_id, cost in cls._purchase_cost(works).items(): costs[work_id] += cost for work in works: costs[work.id] = work.company.currency.round(costs[work.id]) return costs @classmethod def _purchase_cost(cls, works): 'Compute direct purchase cost' pool = Pool() Currency = pool.get('currency.currency') PurchaseLine = pool.get('purchase.line') InvoiceLine = pool.get('account.invoice.line') Invoice = pool.get('account.invoice') Company = pool.get('company.company') cursor = Transaction().connection.cursor() table = cls.__table__() purchase_line = PurchaseLine.__table__() invoice_line = InvoiceLine.__table__() invoice = Invoice.__table__() company = Company.__table__() amounts = defaultdict(Decimal) work_ids = [w.id for w in works] work2currency = {} iline2work = {} for sub_ids in grouped_slice(work_ids): where = reduce_ids(table.id, sub_ids) cursor.execute(*table.join( purchase_line, condition=purchase_line.work == table.id).join( invoice_line, condition=invoice_line.origin == Concat('purchase.line,', purchase_line.id)).join( invoice, condition=invoice_line.invoice == invoice.id ).select(invoice_line.id, table.id, where=where & ~invoice.state.in_(['draft', 'cancelled']))) iline2work.update(cursor) cursor.execute( *table.join(company, condition=table.company == company.id ).select(table.id, company.currency, where=where)) work2currency.update(cursor) currencies = Currency.browse(set(work2currency.values())) id2currency = {c.id: c for c in currencies} invoice_lines = InvoiceLine.browse(list(iline2work.keys())) for invoice_line in invoice_lines: invoice = invoice_line.invoice work_id = iline2work[invoice_line.id] currency_id = work2currency[work_id] currency = id2currency[currency_id] if currency != invoice.currency: with Transaction().set_context(date=invoice.currency_date): amount = Currency.compute(invoice.currency, invoice_line.amount, currency) else: amount = invoice_line.amount amounts[work_id] += amount return amounts @classmethod def _get_revenue(cls, works): revenues = dict.fromkeys(map(int, works), Decimal(0)) for work in works: if not work.list_price: continue if work.price_list_hour: revenue = work.company.currency.round( work.list_price * Decimal(str(work.effort_hours))) else: revenue = work.list_price revenues[work.id] = work.company.currency.round(revenue) return revenues @fields.depends('company') def on_change_with_currency(self, name=None): if self.company: return self.company.currency.id @fields.depends('product', 'company') def on_change_product(self): pool = Pool() User = pool.get('res.user') ModelData = pool.get('ir.model.data') Uom = pool.get('product.uom') Currency = pool.get('currency.currency') if not self.product: return if self.price_list_hour: hour_uom = Uom(ModelData.get_id('product', 'uom_hour')) self.list_price = Uom.compute_price(self.product.default_uom, self.product.list_price, hour_uom) else: self.list_price = self.product.list_price if self.company: user = User(Transaction().user) if user.company != self.company: if user.company.currency != self.company.currency: self.list_price = Currency.compute(user.company.currency, self.list_price, self.company.currency, round=False) self.list_price = round_price(self.list_price) @property def price_list_hour(self): pool = Pool() ModelData = pool.get('ir.model.data') Category = pool.get('product.uom.category') if not self.product: return time = Category(ModelData.get_id('product', 'uom_cat_time')) return self.product.default_uom_category == time
class AdvancePaymentCondition(ModelSQL, ModelView): "Advance Payment Condition" __name__ = 'sale.advance_payment.condition' _rec_name = 'description' _states = { 'readonly': Eval('sale_state') != 'draft', } sale = fields.Many2One('sale.sale', 'Sale', required=True, ondelete='CASCADE', select=True, states={ 'readonly': ((Eval('sale_state') != 'draft') & Bool(Eval('sale'))), }) description = fields.Char("Description", required=True, states=_states) amount = Monetary("Amount", currency='currency', digits='currency', states=_states) account = fields.Many2One('account.account', "Account", required=True, domain=[ ('type.unearned_revenue', '=', True), ('company', '=', Eval('sale_company')), ], states=_states) block_supply = fields.Boolean("Block Supply", states=_states) block_shipping = fields.Boolean("Block Shipping", states=_states) invoice_delay = fields.TimeDelta("Invoice Delay", states=_states) invoice_lines = fields.One2Many('account.invoice.line', 'origin', "Invoice Lines", readonly=True) completed = fields.Function(fields.Boolean("Completed"), 'get_completed') sale_state = fields.Function( fields.Selection('get_sale_states', "Sale State"), 'on_change_with_sale_state') sale_company = fields.Function( fields.Many2One('company.company', "Company"), 'on_change_with_sale_company') currency = fields.Function( fields.Many2One('currency.currency', "Currency"), 'on_change_with_currency') del _states @classmethod def __setup__(cls): super(AdvancePaymentCondition, cls).__setup__() cls._order.insert(0, ('amount', 'ASC')) @classmethod def get_sale_states(cls): pool = Pool() Sale = pool.get('sale.sale') return Sale.fields_get(['state'])['state']['selection'] @fields.depends('sale', '_parent_sale.state') def on_change_with_sale_state(self, name=None): if self.sale: return self.sale.state @fields.depends('sale', '_parent_sale.company') def on_change_with_sale_company(self, name=None): if self.sale and self.sale.company: return self.sale.company.id @fields.depends('sale', '_parent_sale.currency') def on_change_with_currency(self, name=None): if self.sale and self.sale.currency: return self.sale.currency.id @classmethod def copy(cls, conditions, default=None): if default is None: default = {} else: default = default.copy() default.setdefault('invoice_lines', []) return super(AdvancePaymentCondition, cls).copy(conditions, default) def create_invoice(self): invoice = self.sale._get_invoice_sale() invoice.invoice_date = self.sale.sale_date if self.invoice_delay: invoice.invoice_date += self.invoice_delay invoice.payment_term = None invoice_lines = self.get_invoice_advance_payment_lines(invoice) if not invoice_lines: return None invoice.lines = invoice_lines invoice.save() invoice.update_taxes() return invoice def get_invoice_advance_payment_lines(self, invoice): pool = Pool() InvoiceLine = pool.get('account.invoice.line') advance_amount = self._get_advance_amount() advance_amount += self._get_ignored_amount() if advance_amount >= self.amount: return [] invoice_line = InvoiceLine() invoice_line.invoice = invoice invoice_line.type = 'line' invoice_line.quantity = 1 invoice_line.account = self.account invoice_line.unit_price = self.amount - advance_amount invoice_line.description = self.description invoice_line.origin = self invoice_line.company = self.sale.company invoice_line.currency = self.sale.currency # Set taxes invoice_line.on_change_account() return [invoice_line] def _get_advance_amount(self): return sum(l.amount for l in self.invoice_lines if l.invoice.state != 'cancelled') def _get_ignored_amount(self): skips = {l for i in self.sale.invoices_recreated for l in i.lines} return sum(l.amount for l in self.invoice_lines if l.invoice.state == 'cancelled' and l not in skips) def get_completed(self, name): advance_amount = 0 lines_ignored = set(l for i in self.sale.invoices_ignored for l in i.lines) for l in self.invoice_lines: if l.invoice.state == 'paid' or l in lines_ignored: advance_amount += l.amount return advance_amount >= self.amount
class CategoryTree(ModelSQL, ModelView): "Stock Reporting Margin per Category" __name__ = 'stock.reporting.margin.category.tree' name = fields.Function(fields.Char("Name"), 'get_name') parent = fields.Many2One('stock.reporting.margin.category.tree', "Parent") children = fields.One2Many('stock.reporting.margin.category.tree', 'parent', "Children") cost = fields.Function( Monetary(lazy_gettext('stock.msg_stock_reporting_cost'), currency='currency', digits='currency'), 'get_total') revenue = fields.Function( Monetary(lazy_gettext('stock.msg_stock_reporting_revenue'), currency='currency', digits='currency'), 'get_total') profit = fields.Function( Monetary(lazy_gettext('stock.msg_stock_reporting_profit'), currency='currency', digits='currency'), 'get_total') margin = fields.Function( Monetary(lazy_gettext('stock.msg_stock_reporting_margin'), digits=(14, 4)), 'get_margin') currency = fields.Function( fields.Many2One('currency.currency', lazy_gettext('stock.msg_stock_reporting_currency')), 'get_currency') @classmethod def __setup__(cls): super().__setup__() cls._order.insert(0, ('name', 'ASC')) @classmethod def table_query(cls): pool = Pool() Category = pool.get('product.category') return Category.__table__() @classmethod def get_name(cls, categories, name): pool = Pool() Category = pool.get('product.category') categories = Category.browse(categories) return {c.id: c.name for c in categories} @classmethod def order_name(cls, tables): pool = Pool() Category = pool.get('product.category') table, _ = tables[None] if 'category' not in tables: category = Category.__table__() tables['category'] = { None: (category, table.id == category.id), } return Category.name.convert_order('name', tables['category'], Category) def time_series_all(self): return [] @classmethod def get_total(cls, categories, names): pool = Pool() ReportingCategory = pool.get('stock.reporting.margin.category') table = cls.__table__() reporting_category = ReportingCategory.__table__() cursor = Transaction().connection.cursor() categories = cls.search([ ('parent', 'child_of', [c.id for c in categories]), ]) ids = [c.id for c in categories] parents = {} reporting_categories = [] for sub_ids in grouped_slice(ids): sub_ids = list(sub_ids) where = reduce_ids(table.id, sub_ids) cursor.execute(*table.select(table.id, table.parent, where=where)) parents.update(cursor) where = reduce_ids(reporting_category.id, sub_ids) cursor.execute( *reporting_category.select(reporting_category.id, where=where)) reporting_categories.extend(r for r, in cursor) result = {} reporting_categories = ReportingCategory.browse(reporting_categories) for name in names: values = dict.fromkeys(ids, 0) values.update( (c.id, getattr(c, name)) for c in reporting_categories) result[name] = cls._sum_tree(categories, values, parents) return result @classmethod def _sum_tree(cls, categories, values, parents): result = values.copy() categories = set((c.id for c in categories)) leafs = categories - set(parents.values()) while leafs: for category in leafs: categories.remove(category) parent = parents.get(category) if parent in result: result[parent] += result[category] next_leafs = set(categories) for category in categories: parent = parents.get(category) if not parent: continue if parent in next_leafs and parent in categories: next_leafs.remove(parent) leafs = next_leafs return result def get_margin(self, name): digits = self.__class__.margin.digits if self.profit is not None and self.revenue: return (self.profit / self.revenue).quantize( Decimal(1) / 10**digits[1]) def get_currency(self, name): pool = Pool() Company = pool.get('company.company') company = Transaction().context.get('company') if company: return Company(company).currency.id @classmethod def view_attributes(cls): return super().view_attributes() + [ ('/tree/field[@name="profit"]', 'visual', If(Eval('profit', 0) < 0, 'danger', '')), ('/tree/field[@name="margin"]', 'visual', If(Eval('margin', 0) < 0, 'danger', '')), ]
class ProductCategoryTree(ModelSQL, ModelView): "Sale Reporting per Product Category" __name__ = 'sale.reporting.product.category.tree' name = fields.Function(fields.Char("Name"), 'get_name') parent = fields.Many2One('sale.reporting.product.category.tree', "Parent") children = fields.One2Many('sale.reporting.product.category.tree', 'parent', "Children") revenue = fields.Function(Monetary("Revenue", digits='currency'), 'get_total') currency = fields.Function( fields.Many2One('currency.currency', "Currency"), 'get_currency') @classmethod def __setup__(cls): super().__setup__() cls._order.insert(0, ('name', 'ASC')) @classmethod def table_query(cls): pool = Pool() Category = pool.get('product.category') return Category.__table__() @classmethod def get_name(cls, categories, name): pool = Pool() Category = pool.get('product.category') categories = Category.browse(categories) return {c.id: c.name for c in categories} @classmethod def order_name(cls, tables): pool = Pool() Category = pool.get('product.category') table, _ = tables[None] if 'category' not in tables: category = Category.__table__() tables['category'] = { None: (category, table.id == category.id), } return Category.name.convert_order('name', tables['category'], Category) def time_series_all(self): return [] @classmethod def get_total(cls, categories, names): pool = Pool() ReportingProductCategory = pool.get('sale.reporting.product.category') table = cls.__table__() reporting_product_category = ReportingProductCategory.__table__() cursor = Transaction().connection.cursor() categories = cls.search([ ('parent', 'child_of', [c.id for c in categories]), ]) ids = [c.id for c in categories] parents = {} reporting_product_categories = [] for sub_ids in grouped_slice(ids): sub_ids = list(sub_ids) where = reduce_ids(table.id, sub_ids) cursor.execute(*table.select(table.id, table.parent, where=where)) parents.update(cursor) where = reduce_ids(reporting_product_category.id, sub_ids) cursor.execute(*reporting_product_category.select( reporting_product_category.id, where=where)) reporting_product_categories.extend(r for r, in cursor) result = {} reporting_product_categories = ReportingProductCategory.browse( reporting_product_categories) for name in names: values = dict.fromkeys(ids, 0) values.update( (c.id, getattr(c, name)) for c in reporting_product_categories) result[name] = cls._sum_tree(categories, values, parents) return result @classmethod def _sum_tree(cls, categories, values, parents): result = values.copy() categories = set((c.id for c in categories)) leafs = categories - set(parents.values()) while leafs: for category in leafs: categories.remove(category) parent = parents.get(category) if parent in result: result[parent] += result[category] next_leafs = set(categories) for category in categories: parent = parents.get(category) if not parent: continue if parent in next_leafs and parent in categories: next_leafs.remove(parent) leafs = next_leafs return result def get_currency(self, name): pool = Pool() Company = pool.get('company.company') company = Transaction().context.get('company') if company: return Company(company).currency.id
class StatementRule(sequence_ordered(), ModelSQL, ModelView): "Account Statement Rule" __name__ = 'account.statement.rule' name = fields.Char("Name") company = fields.Many2One('company.company', "Company") journal = fields.Many2One('account.statement.journal', "Journal", domain=[ If(Eval('company'), ('company', '=', Eval('company')), ()), ], depends=['company']) amount_low = Monetary("Amount Low", currency='currency', digits='currency', domain=[ If(Eval('amount_high'), [ 'OR', ('amount_low', '=', None), ('amount_low', '<=', Eval('amount_high')), ], []) ], depends=['amount_high']) amount_high = Monetary("Amount High", currency='currency', digits='currency', domain=[ If(Eval('amount_low'), [ 'OR', ('amount_high', '=', None), ('amount_high', '>=', Eval('amount_low')), ], []) ], depends=['amount_low']) description = fields.Char( "Description", help="The regular expression the description is searched with.\n" "It may define the groups named:\n" "'party', 'bank_account', 'invoice'.") information_rules = fields.One2Many('account.statement.rule.information', 'rule', "Information Rules") lines = fields.One2Many('account.statement.rule.line', 'rule', "Lines") currency = fields.Function( fields.Many2One('currency.currency', "Currency"), 'on_change_with_currency') @fields.depends('journal') def on_change_with_currency(self, name=None): if self.journal: return self.journal.currency.id def match(self, origin): keywords = {} if self.company and self.company != origin.company: return False if self.journal and self.journal != origin.statement.journal: return False if self.amount_low is not None and self.amount_low > origin.amount: return False if self.amount_high is not None and self.amount_high < origin.amount: return False if self.information_rules: for irule in self.information_rules: result = irule.match(origin) if isinstance(result, dict): keywords.update(result) elif not result: return False if self.description: result = re.search(self.description, origin.description or '') if not result: return False keywords.update(result.groupdict()) keywords.update(amount=origin.amount, pending=origin.amount) return keywords def apply(self, origin, keywords): keywords = keywords.copy() for rule_line in self.lines: line = rule_line.get_line(origin, keywords) if not line: return keywords['pending'] -= line.amount yield line
class Promotion( DeactivableMixin, ModelSQL, ModelView, MatchMixin): 'Sale Promotion' __name__ = 'sale.promotion' name = fields.Char('Name', translate=True, required=True) company = fields.Many2One( 'company.company', "Company", required=True, select=True, states={ 'readonly': Eval('id', 0) > 0, }) start_date = fields.Date('Start Date', domain=['OR', ('start_date', '<=', If(~Eval('end_date', None), datetime.date.max, Eval('end_date', datetime.date.max))), ('start_date', '=', None), ]) end_date = fields.Date('End Date', domain=['OR', ('end_date', '>=', If(~Eval('start_date', None), datetime.date.min, Eval('start_date', datetime.date.min))), ('end_date', '=', None), ]) price_list = fields.Many2One('product.price_list', 'Price List', ondelete='CASCADE', domain=[ ('company', '=', Eval('company', -1)), ]) amount = Monetary("Amount", currency='currency', digits='currency') currency = fields.Many2One( 'currency.currency', "Currency", states={ 'required': Bool(Eval('amount', 0)), }) untaxed_amount = fields.Boolean( "Untaxed Amount", states={ 'invisible': ~Eval('amount'), }) quantity = fields.Float('Quantity', digits='unit') unit = fields.Many2One('product.uom', 'Unit', states={ 'required': Bool(Eval('quantity', 0)), }) products = fields.Many2Many( 'sale.promotion-product.product', 'promotion', 'product', "Products", context={ 'company': Eval('company', -1), }, depends={'company'}) categories = fields.Many2Many( 'sale.promotion-product.category', 'promotion', 'category', "Categories", context={ 'company': Eval('company', -1), }, depends={'company'}) formula = fields.Char('Formula', required=True, help=('Python expression that will be evaluated with:\n' '- unit_price: the original unit_price')) @staticmethod def default_company(): return Transaction().context.get('company') @classmethod def default_untaxed_amount(cls): return False @classmethod def validate_fields(cls, promotions, field_names): super().validate_fields(promotions, field_names) cls.check_formula(promotions, field_names) @classmethod def check_formula(cls, promotions, field_names=None): if field_names and 'formula' not in field_names: return for promotion in promotions: context = promotion.get_context_formula(None) try: unit_price = promotion.get_unit_price(**context) if not isinstance(unit_price, Decimal): raise ValueError('Not a Decimal') except Exception as exception: raise FormulaError( gettext('sale_promotion.msg_invalid_formula', formula=promotion.formula, promotion=promotion.rec_name, exception=exception)) from exception @classmethod def _promotions_domain(cls, sale): pool = Pool() Date = pool.get('ir.date') with Transaction().set_context(company=sale.company.id): sale_date = sale.sale_date or Date.today() return [ ['OR', ('start_date', '<=', sale_date), ('start_date', '=', None), ], ['OR', ('end_date', '=', None), ('end_date', '>=', sale_date), ], ['OR', ('price_list', '=', None), ('price_list', '=', sale.price_list.id if sale.price_list else None), ], ('company', '=', sale.company.id), ] @classmethod def get_promotions(cls, sale, pattern=None): 'Yield promotions that apply to sale' promotions = cls.search(cls._promotions_domain(sale)) if pattern is None: pattern = {} for promotion in promotions: ppattern = pattern.copy() ppattern.update(promotion.get_pattern(sale)) if promotion.match(ppattern): yield promotion def get_pattern(self, sale): pool = Pool() Currency = pool.get('currency.currency') Uom = pool.get('product.uom') Sale = pool.get('sale.sale') pattern = {} if self.currency: amount = self.get_sale_amount(Sale(sale.id)) pattern['amount'] = Currency.compute( sale.currency, amount, self.currency) if self.unit: quantity = 0 for line in sale.lines: if self.is_valid_sale_line(line): quantity += Uom.compute_qty(line.unit, line.quantity, self.unit) pattern['quantity'] = quantity return pattern def match(self, pattern): def sign(amount): return Decimal(1).copy_sign(amount) if 'quantity' in pattern: pattern = pattern.copy() if (self.quantity or 0) > pattern.pop('quantity'): return False if 'amount' in pattern: pattern = pattern.copy() amount = pattern.pop('amount') if (sign(self.amount or 0) * sign(amount) >= 0 and abs(self.amount or 0) > abs(amount)): return False return super().match(pattern) def get_sale_amount(self, sale): if self.untaxed_amount: return sale.untaxed_amount else: return sale.total_amount def is_valid_sale_line(self, line): def parents(categories): for category in categories: while category: yield category category = category.parent if line.quantity <= 0 or line.unit_price <= 0: return False elif self.unit and line.unit.category != self.unit.category: return False elif self.products and line.product not in self.products: return False elif (self.categories and not set(parents(line.product.categories_all)).intersection( self.categories)): return False else: return True def apply(self, sale): applied = False for line in sale.lines: if line.type != 'line': continue if not self.is_valid_sale_line(line): continue context = self.get_context_formula(line) new_price = self.get_unit_price(**context) if new_price is not None: if new_price < 0: new_price = Decimal(0) if line.unit_price >= new_price: line.unit_price = round_price(new_price) line.promotion = self applied = True if applied: sale.lines = sale.lines # Trigger the change def get_context_formula(self, sale_line): pool = Pool() Product = pool.get('product.product') if sale_line: with Transaction().set_context( sale_line._get_context_sale_price()): prices = Product.get_sale_price([sale_line.product]) unit_price = prices[sale_line.product.id] else: unit_price = Decimal(0) return { 'names': { 'unit_price': unit_price if unit_price is not None else Null(), }, } def get_unit_price(self, **context): 'Return unit price (as Decimal)' context.setdefault('functions', {})['Decimal'] = Decimal return max(simple_eval(decistmt(self.formula), **context), Decimal(0))
class Dunning(ModelSQL, ModelView): 'Account Dunning' __name__ = 'account.dunning' company = fields.Many2One('company.company', 'Company', required=True, help="Make the dunning belong to the company.", select=True, states=_STATES) line = fields.Many2One('account.move.line', 'Line', required=True, help="The receivable line to dun for.", domain=[ ('account.type.receivable', '=', True), ('account.company', '=', Eval('company', -1)), [ 'OR', ('debit', '>', 0), ('credit', '<', 0), ], ], states=_STATES) procedure = fields.Many2One('account.dunning.procedure', 'Procedure', required=True, states=_STATES) level = fields.Many2One('account.dunning.level', 'Level', required=True, domain=[ ('procedure', '=', Eval('procedure', -1)), ], states=_STATES) date = fields.Date("Date", readonly=True, states={ 'invisible': Eval('state') == 'draft', }, help="When the dunning reached the level.") age = fields.Function( fields.TimeDelta("Age", states={ 'invisible': Eval('state') == 'draft', }, help="How long the dunning has been at the level."), 'get_age') blocked = fields.Boolean( 'Blocked', help="Check to block further levels of the procedure.") state = fields.Selection([ ('draft', 'Draft'), ('waiting', "Waiting"), ('final', "Final"), ], 'State', readonly=True, sort=False) active = fields.Function(fields.Boolean('Active'), 'get_active', searcher='search_active') party = fields.Function(fields.Many2One('party.party', 'Party', context={ 'company': Eval('company', -1), }, depends={'company'}), 'get_line_field', searcher='search_line_field') amount = fields.Function( Monetary("Amount", currency='currency', digits='currency'), 'get_amount') currency = fields.Function( fields.Many2One('currency.currency', "Currency"), 'get_line_field') maturity_date = fields.Function(fields.Date('Maturity Date'), 'get_line_field', searcher='search_line_field') amount_second_currency = fields.Function( Monetary('Amount Second Currency', currency='second_currency', digits='second_currency', states={ 'invisible': Eval('currency') == Eval('second_currency'), }), 'get_amount_second_currency') second_currency = fields.Function( fields.Many2One('currency.currency', 'Second Currency'), 'get_second_currency') @classmethod def __setup__(cls): super(Dunning, cls).__setup__() table = cls.__table__() cls._sql_constraints = [ ('line_unique', Unique(table, table.line), 'account_dunning.msg_dunning_line_unique'), ] cls._active_field = 'active' cls._buttons.update({ 'reschedule': { 'invisible': ~Eval('active', True), 'depends': ['active'], }, }) @classmethod def __register__(cls, module): dunning = cls.__table__() super().__register__(module) cursor = Transaction().connection.cursor() # Migration from 4.8: rename done state into waiting cursor.execute(*dunning.update([dunning.state], ['waiting'], where=dunning.state == 'done')) @staticmethod def default_company(): return Transaction().context.get('company') @staticmethod def default_state(): return 'draft' def get_age(self, name): pool = Pool() Date = pool.get('ir.date') if self.date: with Transaction().set_context(company=self.company.id): return Date.today() - self.date @classmethod def order_age(cls, tables): pool = Pool() Date = pool.get('ir.date') table, _ = tables[None] return [Literal(Date.today()) - table.date] def get_active(self, name): return not self.line.reconciliation def get_line_field(self, name): value = getattr(self.line, name) if isinstance(value, Model): return value.id else: return value @classmethod def search_line_field(cls, name, clause): return [('line.' + clause[0], ) + tuple(clause[1:])] def get_amount(self, name): return self.line.debit - self.line.credit def get_amount_second_currency(self, name): amount = self.line.debit - self.line.credit if self.line.amount_second_currency: return self.line.amount_second_currency.copy_sign(amount) else: return amount def get_second_currency(self, name): if self.line.second_currency: return self.line.second_currency.id else: return self.line.account.company.currency.id @classmethod def search_active(cls, name, clause): reverse = { '=': '!=', '!=': '=', } if clause[1] in reverse: if clause[2]: return [('line.reconciliation', clause[1], None)] else: return [('line.reconciliation', reverse[clause[1]], None)] else: return [] @classmethod def _overdue_line_domain(cls, date): return [ ('account.type.receivable', '=', True), ('dunnings', '=', None), ('maturity_date', '<=', date), [ 'OR', ('debit', '>', 0), ('credit', '<', 0), ], ('party', '!=', None), ('reconciliation', '=', None), ] @classmethod def generate_dunnings(cls, date=None): pool = Pool() Date = pool.get('ir.date') MoveLine = pool.get('account.move.line') if date is None: date = Date.today() set_level = defaultdict(list) with Transaction().set_context(_check_access=True): dunnings = cls.search([ ('state', '=', 'waiting'), ('blocked', '=', False), ]) dunnings = cls.browse(dunnings) for dunning in dunnings: procedure = dunning.procedure levels = procedure.levels levels = levels[levels.index(dunning.level) + 1:] if levels: for level in levels: if level.test(dunning.line, date): break else: level = dunning.level if level != dunning.level: set_level[level].append(dunning) else: set_level[None].append(dunning) to_write = [] for level, dunnings in set_level.items(): if level: to_write.extend((dunnings, { 'level': level.id, 'state': 'draft', 'date': None, })) else: to_write.extend((dunnings, { 'state': 'final', 'date': Date.today(), })) if to_write: cls.write(*to_write) with Transaction().set_context(_check_access=True): lines = MoveLine.search(cls._overdue_line_domain(date)) lines = MoveLine.browse(lines) dunnings = (cls._get_dunning(line, date) for line in lines) cls.save([d for d in dunnings if d]) @classmethod def _get_dunning(cls, line, date): procedure = line.dunning_procedure if not procedure: return for level in procedure.levels: if level.test(line, date): break else: return return cls( line=line, procedure=procedure, level=level, ) @classmethod def process(cls, dunnings): pool = Pool() Date = pool.get('ir.date') for company, c_dunnings in groupby(dunnings, key=lambda d: d.company): with Transaction().set_context(company=company.id): today = Date.today() cls.write([ d for d in c_dunnings if not d.blocked and d.state == 'draft' ], { 'state': 'waiting', 'date': today, }) @classmethod @ModelView.button_action('account_dunning.act_reschedule_dunning_wizard') def reschedule(cls, dunnings): pass
class QuotationLine(ModelSQL, ModelView): "Purchase Request For Quotation Line" __name__ = 'purchase.request.quotation.line' supplier = fields.Function(fields.Many2One('party.party', 'Supplier'), 'get_supplier') supply_date = fields.Date('Supply Date', help="When it should be delivered.") product = fields.Function(fields.Many2One('product.product', 'Product'), 'get_product', searcher='search_product') description = fields.Text('Description', states={'required': ~Eval('product')}) quantity = fields.Float("Quantity", digits='unit', required=True) unit = fields.Many2One( 'product.uom', 'Unit', ondelete='RESTRICT', states={ 'required': Bool(Eval('product')), }, domain=[ If(Bool(Eval('product_uom_category')), ('category', '=', Eval('product_uom_category')), ('category', '!=', -1)), ]) product_uom_category = fields.Function( fields.Many2One('product.uom.category', 'Product Uom Category'), 'on_change_with_product_uom_category') unit_price = Monetary("Unit Price", currency='currency', digits=price_digits) currency = fields.Many2One('currency.currency', 'Currency', states={ 'required': Bool(Eval('unit_price')), }) request = fields.Many2One( 'purchase.request', 'Request', ondelete='CASCADE', select=True, required=True, domain=[ If( Eval('quotation_state') == 'draft', ('state', 'in', ['draft', 'quotation', 'received']), (), ), ], states={'readonly': Eval('quotation_state') != 'draft'}, help="The request which this line belongs to.") quotation = fields.Many2One('purchase.request.quotation', 'Quotation', ondelete='CASCADE', required=True, domain=[ ('supplier', '=', Eval('supplier')), ]) quotation_state = fields.Function(fields.Selection('get_quotation_state', 'Quotation State'), 'on_change_with_quotation_state', searcher='search_quotation_state') @classmethod def __setup__(cls): super().__setup__() cls.__access__.add('quotation') @staticmethod def order_quotation_state(tables): pool = Pool() Quotation = pool.get('purchase.request.quotation') quotation_line, _ = tables[None] quotation = Quotation.__table__() tables['purchase.request.quotation'] = { None: (quotation, quotation_line.quotation == quotation.id), } return [ Case((quotation.state == 'received', 0), else_=1), quotation.state ] def get_supplier(self, name): if self.quotation and self.quotation.supplier: return self.quotation.supplier.id @fields.depends('request', '_parent_request.product', '_parent_request.description', '_parent_request.quantity', '_parent_request.uom', '_parent_request.company', '_parent_request.supply_date') def on_change_request(self): if self.request: self.product = self.request.product self.description = self.request.description self.quantity = self.request.quantity self.unit = self.request.uom if self.request.company: self.currency = self.request.company.currency self.supply_date = self.request.supply_date or datetime.date.max @fields.depends('product') def on_change_with_product_uom_category(self, name=None): if self.product: return self.product.default_uom_category.id @classmethod def get_quotation_state(cls): pool = Pool() Quotation = pool.get('purchase.request.quotation') return (Quotation.fields_get(['state'])['state']['selection']) @fields.depends('quotation', '_parent_quotation.state') def on_change_with_quotation_state(self, name=None): pool = Pool() Quotation = pool.get('purchase.request.quotation') if self.quotation: return self.quotation.state return Quotation.default_state() @classmethod def search_quotation_state(cls, name, clause): return [('quotation.state', ) + tuple(clause[1:])] def get_rec_name(self, name): return '%s - %s' % (self.quotation.rec_name, self.supplier.rec_name) @classmethod def search_rec_name(cls, name, clause): names = clause[2].split(' - ', 1) res = [('quotation', clause[1], names[0])] if len(names) != 1 and names[1]: res.append(('supplier', clause[1], names[1])) return res @classmethod def delete(cls, quotationlines): pool = Pool() Request = pool.get('purchase.request') requests = [l.request for l in quotationlines] super(QuotationLine, cls).delete(quotationlines) Request.update_state(requests) def get_product(self, name): if self.request and self.request.product: return self.request.product.id @classmethod def search_product(cls, name, clause): return [('request.' + clause[0], ) + tuple(clause[1:])]
class Product(metaclass=PoolMeta): __name__ = 'product.product' sale_price_uom = fields.Function(Monetary( "Sale Price", digits=price_digits), 'get_sale_price_uom') @classmethod def get_sale_price_uom(cls, products, name): quantity = Transaction().context.get('quantity') or 0 return cls.get_sale_price(products, quantity=quantity) def _get_sale_unit_price(self, quantity=0): return self.list_price_used @classmethod def get_sale_price(cls, products, quantity=0): ''' Return the sale price for products and quantity. It uses if exists from the context: uom: the unit of measure or the sale uom of the product currency: the currency id for the returned price ''' pool = Pool() Uom = pool.get('product.uom') User = pool.get('res.user') Currency = pool.get('currency.currency') Date = pool.get('ir.date') today = Date.today() prices = {} assert len(products) == len(set(products)), "Duplicate products" uom = None if Transaction().context.get('uom'): uom = Uom(Transaction().context.get('uom')) currency = None if Transaction().context.get('currency'): currency = Currency(Transaction().context.get('currency')) user = User(Transaction().user) for product in products: unit_price = product._get_sale_unit_price(quantity=quantity) if unit_price is not None: if uom and product.default_uom.category == uom.category: unit_price = Uom.compute_price( product.default_uom, unit_price, uom) else: unit_price = Uom.compute_price( product.default_uom, unit_price, product.sale_uom) if currency and user.company and unit_price is not None: if user.company.currency != currency: date = Transaction().context.get('sale_date') or today with Transaction().set_context(date=date): unit_price = Currency.compute( user.company.currency, unit_price, currency, round=False) prices[product.id] = unit_price return prices @property def lead_time_used(self): pool = Pool() Configuration = pool.get('product.configuration') if self.lead_time is None: with Transaction().set_context(self._context): config = Configuration(1) return config.get_multivalue('default_lead_time') else: return self.lead_time def compute_shipping_date(self, date=None): ''' Compute the shipping date at the given date ''' Date = Pool().get('ir.date') if not date: with Transaction().set_context(context=self._context): date = Date.today() lead_time = self.lead_time_used if lead_time is None: return datetime.date.max return date + lead_time
class Journal(DeactivableMixin, ModelSQL, ModelView, CompanyMultiValueMixin): 'Journal' __name__ = 'account.journal' name = fields.Char('Name', size=None, required=True, translate=True) code = fields.Char('Code', size=None) type = fields.Selection([ ('general', "General"), ('revenue', "Revenue"), ('expense', "Expense"), ('cash', "Cash"), ('situation', "Situation"), ('write-off', "Write-Off"), ], 'Type', required=True) sequence = fields.MultiValue( fields.Many2One('ir.sequence', "Sequence", domain=[ ('sequence_type', '=', Id('account', 'sequence_type_account_journal')), ('company', 'in', [Eval('context', {}).get('company', -1), None]), ], states={ 'required': Bool(Eval('context', {}).get('company', -1)), })) sequences = fields.One2Many('account.journal.sequence', 'journal', "Sequences") debit = fields.Function( Monetary("Debit", currency='currency', digits='currency'), 'get_debit_credit_balance') credit = fields.Function( Monetary("Credit", currency='currency', digits='currency'), 'get_debit_credit_balance') balance = fields.Function( Monetary("Balance", currency='currency', digits='currency'), 'get_debit_credit_balance') currency = fields.Function( fields.Many2One('currency.currency', "Currency"), 'get_currency') @classmethod def __setup__(cls): super(Journal, cls).__setup__() cls._order.insert(0, ('name', 'ASC')) @classmethod def default_sequence(cls, **pattern): return cls.multivalue_model('sequence').default_sequence() @classmethod def search_rec_name(cls, name, clause): if clause[1].startswith('!') or clause[1].startswith('not '): bool_op = 'AND' else: bool_op = 'OR' code_value = clause[2] if clause[1].endswith('like'): code_value = lstrip_wildcard(clause[2]) return [ bool_op, ('code', clause[1], code_value) + tuple(clause[3:]), (cls._rec_name, ) + tuple(clause[1:]), ] @classmethod def get_currency(cls, journals, name): pool = Pool() Company = pool.get('company.company') company_id = Transaction().context.get('company') if company_id: company = Company(company_id) currency_id = company.currency.id else: currency_id = None return dict.fromkeys([j.id for j in journals], currency_id) @classmethod def get_debit_credit_balance(cls, journals, names): pool = Pool() MoveLine = pool.get('account.move.line') Move = pool.get('account.move') Account = pool.get('account.account') AccountType = pool.get('account.account.type') Company = pool.get('company.company') context = Transaction().context cursor = Transaction().connection.cursor() result = {} ids = [j.id for j in journals] for name in ['debit', 'credit', 'balance']: result[name] = dict.fromkeys(ids, 0) company_id = Transaction().context.get('company') if not company_id: return result company = Company(company_id) line = MoveLine.__table__() move = Move.__table__() account = Account.__table__() account_type = AccountType.__table__() where = ((move.date >= context.get('start_date')) & (move.date <= context.get('end_date')) & ~account_type.receivable & ~account_type.payable & (move.company == company.id)) for sub_journals in grouped_slice(journals): sub_journals = list(sub_journals) red_sql = reduce_ids(move.journal, [j.id for j in sub_journals]) query = line.join( move, 'LEFT', condition=line.move == move.id).join( account, 'LEFT', condition=line.account == account.id).join( account_type, 'LEFT', condition=account.type == account_type.id).select( move.journal, Sum(line.debit), Sum(line.credit), where=where & red_sql, group_by=move.journal) cursor.execute(*query) for journal_id, debit, credit in cursor: # SQLite uses float for SUM if not isinstance(debit, Decimal): debit = Decimal(str(debit)) if not isinstance(credit, Decimal): credit = Decimal(str(credit)) result['debit'][journal_id] = company.currency.round(debit) result['credit'][journal_id] = company.currency.round(credit) result['balance'][journal_id] = company.currency.round(debit - credit) return result @classmethod def write(cls, *args): pool = Pool() Move = pool.get('account.move') actions = iter(args) for journals, values in zip(actions, actions): if 'type' in values: for sub_journals in grouped_slice(journals): moves = Move.search( [('journal', 'in', [j.id for j in sub_journals]), ('state', '=', 'posted')], order=[], limit=1) if moves: move, = moves raise AccessError( gettext('account.msg_journal_account_moves', journal=move.journal.rec_name)) super().write(*args)
class Asset(Workflow, ModelSQL, ModelView): 'Asset' __name__ = 'account.asset' _rec_name = 'number' number = fields.Char('Number', readonly=True, select=True) product = fields.Many2One('product.product', 'Product', required=True, states={ 'readonly': (Eval('lines', [0]) | (Eval('state') != 'draft')), }, context={ 'company': Eval('company', None), }, depends={'company'}, domain=[ ('type', '=', 'assets'), ('depreciable', '=', True), ]) supplier_invoice_line = fields.Many2One( 'account.invoice.line', 'Supplier Invoice Line', domain=[ If( ~Eval('product', None), ('product', '=', -1), ('product', '=', Eval('product', -1)), ), ('invoice.type', '=', 'in'), [ 'OR', ('company', '=', Eval('company', -1)), ('invoice.company', '=', Eval('company', -1)), ], ], states={ 'readonly': (Eval('lines', [0]) | (Eval('state') != 'draft')), }) customer_invoice_line = fields.Function( fields.Many2One('account.invoice.line', 'Customer Invoice Line'), 'get_customer_invoice_line') account_journal = fields.Many2One('account.journal', 'Journal', states={ 'readonly': Eval('state') != 'draft', }, domain=[('type', '=', 'asset')], required=True) company = fields.Many2One('company.company', 'Company', states={ 'readonly': Eval('state') != 'draft', }, required=True) currency = fields.Function( fields.Many2One('currency.currency', 'Currency'), 'on_change_with_currency') quantity = fields.Float("Quantity", digits='unit', states={ 'readonly': (Bool(Eval('supplier_invoice_line', 1)) | Eval('lines', [0]) | (Eval('state') != 'draft')), }) unit = fields.Many2One('product.uom', 'Unit', states={ 'readonly': (Bool(Eval('product')) | (Eval('state') != 'draft')), }) value = Monetary("Value", currency='currency', digits='currency', states={ 'readonly': (Eval('lines', [0]) | (Eval('state') != 'draft')), }, required=True, help="The value of the asset when purchased.") depreciated_amount = Monetary( "Depreciated Amount", currency='currency', digits='currency', domain=[ ('depreciated_amount', '<=', Eval('value')), ], states={ 'readonly': (Eval('lines', [0]) | (Eval('state') != 'draft')), }, required=True, help="The amount already depreciated at the start date.") depreciating_value = fields.Function( Monetary("Depreciating Value", currency='currency', digits='currency', help="The value of the asset at the start date."), 'on_change_with_depreciating_value') residual_value = Monetary( "Residual Value", currency='currency', digits='currency', required=True, domain=[ ('residual_value', '<=', Eval('depreciating_value')), ], states={ 'readonly': (Eval('lines', [0]) | (Eval('state') != 'draft')), }) purchase_date = fields.Date('Purchase Date', states={ 'readonly': (Bool(Eval('supplier_invoice_line', 1)) | Eval('lines', [0]) | (Eval('state') != 'draft')), }, required=True) start_date = fields.Date( 'Start Date', states={ 'readonly': (Eval('lines', [0]) | (Eval('state') != 'draft')), }, required=True, domain=[('start_date', '<=', Eval('end_date', None))]) end_date = fields.Date( 'End Date', states={ 'readonly': (Eval('lines', [0]) | (Eval('state') != 'draft')), }, required=True, domain=[('end_date', '>=', Eval('start_date', None))]) depreciation_method = fields.Selection( [ ('linear', 'Linear'), ], 'Depreciation Method', states={ 'readonly': (Eval('lines', [0]) | (Eval('state') != 'draft')), }, required=True) frequency = fields.Selection([ ('monthly', 'Monthly'), ('yearly', 'Yearly'), ], 'Frequency', required=True, states={ 'readonly': (Eval('lines', [0]) | (Eval('state') != 'draft')), }) state = fields.Selection([ ('draft', 'Draft'), ('running', 'Running'), ('closed', 'Closed'), ], "State", readonly=True, sort=False) lines = fields.One2Many('account.asset.line', 'asset', 'Lines', readonly=True) move = fields.Many2One('account.move', 'Account Move', readonly=True, domain=[ ('company', '=', Eval('company', -1)), ]) update_moves = fields.Many2Many('account.asset-update-account.move', 'asset', 'move', 'Update Moves', readonly=True, domain=[ ('company', '=', Eval('company', -1)), ], states={ 'invisible': ~Eval('update_moves'), }) comment = fields.Text('Comment') revisions = fields.One2Many('account.asset.revision', 'asset', "Revisions", readonly=True) @classmethod def __setup__(cls): super(Asset, cls).__setup__() table = cls.__table__() cls._sql_constraints = [ ('invoice_line_uniq', Unique(table, table.supplier_invoice_line), 'account_asset.msg_asset_invoice_line_unique'), ] cls._transitions |= set(( ('draft', 'running'), ('running', 'closed'), ('running', 'draft'), )) cls._buttons.update({ 'draft': { 'invisible': (Eval('lines', []) | (Eval('state') != 'running')), 'depends': ['state'], }, 'run': { 'invisible': Eval('state') != 'draft', 'depends': ['state'], }, 'close': { 'invisible': Eval('state') != 'running', 'depends': ['state'], }, 'create_lines': { 'invisible': ~Eval('state').in_(['draft', 'running']), 'depends': ['state'], }, 'clear_lines': { 'invisible': (~Eval('lines', [0]) | ~Eval('state').in_(['draft', 'running'])), 'depends': ['state'], }, 'update': { 'invisible': Eval('state') != 'running', 'depends': ['state'], }, }) @classmethod def __register__(cls, module_name): table_h = cls.__table_handler__(module_name) # Migration from 3.8: rename reference into number if table_h.column_exist('reference'): table_h.column_rename('reference', 'number') super(Asset, cls).__register__(module_name) @staticmethod def default_state(): return 'draft' @classmethod def default_frequency(cls, **pattern): pool = Pool() Configuration = pool.get('account.configuration') return Configuration(1).get_multivalue('asset_frequency', **pattern) @staticmethod def default_depreciation_method(): return 'linear' @classmethod def default_depreciated_amount(cls): return Decimal(0) @classmethod def default_residual_value(cls): return Decimal(0) @staticmethod def default_start_date(): return Pool().get('ir.date').today() @staticmethod def default_company(): return Transaction().context.get('company') @fields.depends('company') def on_change_company(self): self.frequency = self.default_frequency( company=self.company.id if self.company else None) @staticmethod def default_account_journal(): Journal = Pool().get('account.journal') journals = Journal.search([ ('type', '=', 'asset'), ]) if len(journals) == 1: return journals[0].id return None @fields.depends('value', 'depreciated_amount') def on_change_with_depreciating_value(self, name=None): if self.value is not None and self.depreciated_amount is not None: return self.value - self.depreciated_amount else: return Decimal(0) @fields.depends('company') def on_change_with_currency(self, name=None): if self.company: return self.company.currency.id @fields.depends('supplier_invoice_line', 'unit') def on_change_supplier_invoice_line(self): pool = Pool() Currency = pool.get('currency.currency') Unit = Pool().get('product.uom') if not self.supplier_invoice_line: self.quantity = None self.value = None self.start_date = self.default_start_date() return invoice_line = self.supplier_invoice_line invoice = invoice_line.invoice if invoice.company.currency != invoice.currency: with Transaction().set_context(date=invoice.currency_date): self.value = Currency.compute(invoice.currency, invoice_line.amount, invoice.company.currency) else: self.value = invoice_line.amount if invoice.invoice_date: self.purchase_date = invoice.invoice_date self.start_date = invoice.invoice_date if invoice_line.product.depreciation_duration: duration = relativedelta.relativedelta( months=invoice_line.product.depreciation_duration, days=-1) self.end_date = self.start_date + duration if not self.unit: self.quantity = invoice_line.quantity else: self.quantity = Unit.compute_qty(invoice_line.unit, invoice_line.quantity, self.unit) @fields.depends('product') def on_change_with_unit(self): if not self.product: return None return self.product.default_uom.id @fields.depends('end_date', 'product', 'start_date') def on_change_with_end_date(self): if (all(getattr(self, k, None) for k in ('product', 'start_date')) and not self.end_date): if self.product.depreciation_duration: duration = relativedelta.relativedelta(months=int( self.product.depreciation_duration), days=-1) return self.start_date + duration return self.end_date @classmethod def get_customer_invoice_line(cls, assets, name): InvoiceLine = Pool().get('account.invoice.line') invoice_lines = InvoiceLine.search([ ('asset', 'in', [a.id for a in assets]), ]) result = dict((a.id, None) for a in assets) result.update(dict((l.asset.id, l.id) for l in invoice_lines)) return result def get_depreciated_amount(self): lines = [ line.depreciation for line in self.lines if line.move and line.move.state == 'posted' ] return sum(lines, Decimal(0)) def compute_move_dates(self): """ Returns all the remaining dates at which asset depreciation movement will be issued. """ pool = Pool() Config = pool.get('account.configuration') config = Config(1) start_date = max([self.start_date] + [l.date for l in self.lines]) delta = relativedelta.relativedelta(self.end_date, start_date) # dateutil >= 2.0 has replace __nonzero__ by __bool__ which doesn't # work in Python < 3 if delta == relativedelta.relativedelta(): if not self.lines: return [self.end_date] else: return [] if self.frequency == 'monthly': rule = rrule.rrule(rrule.MONTHLY, dtstart=self.start_date, bymonthday=int( config.get_multivalue( 'asset_bymonthday', company=self.company.id))) elif self.frequency == 'yearly': rule = rrule.rrule( rrule.YEARLY, dtstart=self.start_date, bymonth=int( config.get_multivalue('asset_bymonth', company=self.company.id)), bymonthday=int( config.get_multivalue('asset_bymonthday', company=self.company.id))) dates = [ d.date() for d in rule.between(date2datetime(start_date), date2datetime(self.end_date)) ] dates.append(self.end_date) return dates def compute_depreciation(self, amount, date, dates): """ Returns the depreciation amount for an asset on a certain date. """ if self.depreciation_method == 'linear': start_date = max( [self.start_date - relativedelta.relativedelta(days=1)] + [l.date for l in self.lines]) first_delta = normalized_delta(start_date, dates[0]) if len(dates) > 1: last_delta = normalized_delta(dates[-2], dates[-1]) else: last_delta = first_delta if self.frequency == 'monthly': _, first_ndays = calendar.monthrange(dates[0].year, dates[0].month) if (calendar.isleap(dates[0].year) and dates[0].month == February): first_ndays -= 1 _, last_ndays = calendar.monthrange(dates[-1].year, dates[-1].month) if (calendar.isleap(dates[-1].year) and dates[-1].month == February): last_ndays -= 1 elif self.frequency == 'yearly': first_ndays = last_ndays = 365 first_ratio = (Decimal(min(first_delta.days, first_ndays)) / Decimal(first_ndays)) last_ratio = (Decimal(min(last_delta.days, last_ndays)) / Decimal(last_ndays)) depreciation = amount / (len(dates) - 2 + first_ratio + last_ratio) if date == dates[0]: depreciation *= first_ratio elif date == dates[-1]: depreciation *= last_ratio return self.company.currency.round(depreciation) def depreciate(self): """ Returns all the depreciation amounts still to be accounted. """ Line = Pool().get('account.asset.line') amounts = {} dates = self.compute_move_dates() depreciated_amount = self.get_depreciated_amount() amount = (self.depreciating_value - depreciated_amount - self.residual_value) if amount <= 0: return amounts residual_value, acc_depreciation = (amount, depreciated_amount + self.depreciated_amount) asset_line = None for date in dates: depreciation = self.compute_depreciation(amount, date, dates) amounts[date] = asset_line = Line( acquired_value=self.value, depreciable_basis=amount, ) if depreciation > residual_value: asset_line.depreciation = residual_value asset_line.accumulated_depreciation = (acc_depreciation + residual_value) break else: residual_value -= depreciation acc_depreciation += depreciation asset_line.depreciation = depreciation asset_line.accumulated_depreciation = acc_depreciation else: if residual_value > 0 and asset_line is not None: asset_line.depreciation += residual_value asset_line.accumulated_depreciation += residual_value for asset_line in amounts.values(): asset_line.actual_value = (self.value - asset_line.accumulated_depreciation) return amounts @classmethod @ModelView.button def create_lines(cls, assets): pool = Pool() Line = pool.get('account.asset.line') lines = [] for asset in assets: for date, line in asset.depreciate().items(): line.asset = asset.id line.date = date lines.append(line) Line.save(lines) @classmethod @ModelView.button def clear_lines(cls, assets): Line = Pool().get('account.asset.line') lines_to_delete = [] for asset in assets: for line in asset.lines: if not line.move or line.move.state != 'posted': lines_to_delete.append(line) Line.delete(lines_to_delete) @classmethod @ModelView.button_action('account_asset.wizard_update') def update(cls, assets): pass def get_move(self, line): """ Return the account.move generated by an asset line. """ pool = Pool() Period = pool.get('account.period') Move = pool.get('account.move') MoveLine = pool.get('account.move.line') period_id = Period.find(self.company.id, line.date) with Transaction().set_context(date=line.date): expense_line = MoveLine( credit=0, debit=line.depreciation, account=self.product.account_expense_used, ) depreciation_line = MoveLine( debit=0, credit=line.depreciation, account=self.product.account_depreciation_used, ) return Move( company=self.company, origin=line, period=period_id, journal=self.account_journal, date=line.date, lines=[expense_line, depreciation_line], ) @classmethod def create_moves(cls, assets, date): """ Creates all account move on assets before a date. """ pool = Pool() Move = pool.get('account.move') Line = pool.get('account.asset.line') cls.create_lines(assets) moves = [] lines = [] for asset_ids in grouped_slice(assets): lines += Line.search([ ('asset', 'in', list(asset_ids)), ('date', '<=', date), ('move', '=', None), ]) for line in lines: moves.append(line.asset.get_move(line)) Move.save(moves) for move, line in zip(moves, lines): line.move = move Line.save(lines) Move.post(moves) def get_closing_move(self, account, date=None): """ Returns closing move values. """ pool = Pool() Period = pool.get('account.period') Date = pool.get('ir.date') Move = pool.get('account.move') MoveLine = pool.get('account.move.line') if date is None: with Transaction().set_context(company=self.company.id): date = Date.today() period_id = Period.find(self.company.id, date) if self.supplier_invoice_line: account_asset = self.supplier_invoice_line.account.current() else: account_asset = self.product.account_asset_used asset_line = MoveLine( debit=0, credit=self.value, account=account_asset, ) depreciation_line = MoveLine( debit=self.get_depreciated_amount() + self.depreciated_amount, credit=0, account=self.product.account_depreciation_used, ) lines = [asset_line, depreciation_line] square_amount = asset_line.credit - depreciation_line.debit if square_amount: if not account: account = self.product.account_revenue_used counter_part_line = MoveLine( debit=square_amount if square_amount > 0 else 0, credit=-square_amount if square_amount < 0 else 0, account=account, ) lines.append(counter_part_line) return Move( company=self.company, origin=self, period=period_id, journal=self.account_journal, date=date, lines=lines, ) @classmethod def set_number(cls, assets): ''' Fill the number field with asset sequence. ''' pool = Pool() Config = pool.get('account.configuration') config = Config(1) for asset in assets: if asset.number: continue asset.number = config.get_multivalue( 'asset_sequence', company=asset.company.id).get() cls.save(assets) @classmethod @ModelView.button @Workflow.transition('draft') def draft(cls, assets): for asset in assets: if asset.lines: raise AccessError( gettext('account_asset.msg_draft_lines', asset=asset.rec_name)) @classmethod @ModelView.button @Workflow.transition('running') def run(cls, assets): cls.set_number(assets) cls.create_lines(assets) @classmethod @ModelView.button @Workflow.transition('closed') def close(cls, assets, account=None, date=None): """ Close the assets. If account is provided, it will be used instead of the expense account. """ Move = Pool().get('account.move') cls.clear_lines(assets) moves = [] for asset in assets: moves.append(asset.get_closing_move(account, date=date)) Move.save(moves) for move, asset in zip(moves, assets): asset.move = move cls.save(assets) Move.post(moves) def get_rec_name(self, name): return '%s - %s' % (self.number, self.product.rec_name) @classmethod def search_rec_name(cls, name, clause): names = clause[2].split(' - ', 1) res = [('number', clause[1], names[0])] if len(names) != 1 and names[1]: res.append(('product', clause[1], names[1])) return res @classmethod def copy(cls, assets, default=None): if default is None: default = {} else: default = default.copy() default.setdefault('lines', []) default.setdefault('update_moves', []) default.setdefault('number', None) default.setdefault('supplier_invoice_line', None) default.setdefault('move') default.setdefault('revisions', []) return super(Asset, cls).copy(assets, default=default) @classmethod def delete(cls, assets): for asset in assets: if asset.state != 'draft': raise AccessError( gettext('account_asset.msg_delete_draft', asset=asset.rec_name)) return super(Asset, cls).delete(assets)
class LineGroup(ModelSQL, ModelView): 'Account Statement Line Group' __name__ = 'account.statement.line.group' _rec_name = 'number' statement = fields.Many2One('account.statement', 'Statement') journal = fields.Function(fields.Many2One('account.statement.journal', 'Journal'), 'get_journal', searcher='search_journal') number = fields.Char('Number') date = fields.Date('Date') amount = Monetary( "Amount", currency='currency', digits='currency') currency = fields.Function(fields.Many2One('currency.currency', 'Currency'), 'get_currency') party = fields.Many2One('party.party', 'Party') move = fields.Many2One('account.move', 'Move') @classmethod def __setup__(cls): super(LineGroup, cls).__setup__() cls.__access__.add('statement') cls._order.insert(0, ('date', 'DESC')) @classmethod def _grouped_columns(cls, line): return [ Max(line.statement).as_('statement'), Max(line.number).as_('number'), Max(line.date).as_('date'), Sum(line.amount).as_('amount'), Max(line.party).as_('party'), ] @classmethod def table_query(cls): pool = Pool() Move = pool.get('account.move') Line = pool.get('account.statement.line') move = Move.__table__() line = Line.__table__() std_columns = [ move.id, move.create_uid, move.create_date, move.write_uid, move.write_date, ] columns = (std_columns + [move.id.as_('move')] + cls._grouped_columns(line)) return move.join(line, condition=move.id == line.move ).select(*columns, where=move.origin.like(Statement.__name__ + ',%'), group_by=std_columns + [move.id] ) def get_journal(self, name): return self.statement.journal.id @classmethod def search_journal(cls, name, clause): return [('statement.' + clause[0],) + tuple(clause[1:])] def get_currency(self, name): return self.statement.journal.currency.id
class Abstract(ModelSQL): company = fields.Many2One('company.company', lazy_gettext("sale.msg_sale_reporting_company")) number = fields.Integer( lazy_gettext("sale.msg_sale_reporting_number"), help=lazy_gettext("sale.msg_sale_reporting_number_help")) revenue = Monetary(lazy_gettext("sale.msg_sale_reporting_revenue"), digits='currency', currency='currency') revenue_trend = fields.Function( fields.Char(lazy_gettext("sale.msg_sale_reporting_revenue_trend")), 'get_trend') time_series = None currency = fields.Function( fields.Many2One('currency.currency', lazy_gettext("sale.msg_sale_reporting_currency")), 'get_currency') @classmethod def table_query(cls): from_item, tables, withs = cls._joins() return from_item.select(*cls._columns(tables, withs), where=cls._where(tables, withs), group_by=cls._group_by(tables, withs), with_=withs.values()) @classmethod def _sale_line(cls, length, index, company_id=None): pool = Pool() Line = pool.get('sale.line') Sale = pool.get('sale.sale') line = Line.__table__() sale = Sale.__table__() return (line.join(sale, condition=line.sale == sale.id).select( (line.id * length + index).as_('id'), line.product.as_('product'), Coalesce(line.actual_quantity, line.quantity).as_('quantity'), line.unit_price.as_('unit_price'), Concat('sale.sale,', line.sale).as_('order'), sale.sale_date.as_('date'), sale.company.as_('company'), sale.currency.as_('currency'), sale.party.as_('customer'), sale.warehouse.as_('location'), sale.shipment_address.as_('shipment_address'), where=sale.state.in_(cls._sale_states()) & (sale.company == company_id), )) @classmethod def _lines(cls): return [cls._sale_line] @classmethod def _joins(cls): pool = Pool() Company = pool.get('company.company') Currency = pool.get('currency.currency') context = Transaction().context tables = {} company = context.get('company') lines = cls._lines() tables['line'] = line = Union(*(l(len(lines), i, company) for i, l in enumerate(lines))) tables['line.company'] = company = Company.__table__() withs = {} currency_sale = With(query=Currency.currency_rate_sql()) withs['currency_sale'] = currency_sale currency_company = With(query=Currency.currency_rate_sql()) withs['currency_company'] = currency_company from_item = (line.join( currency_sale, condition=(line.currency == currency_sale.currency) & (currency_sale.start_date <= line.date) & ((currency_sale.end_date == Null) | (currency_sale.end_date >= line.date))).join( company, condition=line.company == company.id).join( currency_company, condition=(company.currency == currency_company.currency) & (currency_company.start_date <= line.date) & ((currency_company.end_date == Null) | (currency_company.end_date >= line.date)))) return from_item, tables, withs @classmethod def _columns(cls, tables, withs): line = tables['line'] currency_company = withs['currency_company'] currency_sale = withs['currency_sale'] revenue = cls.revenue.sql_cast( Sum(line.quantity * line.unit_price * currency_company.rate / currency_sale.rate)) return [ cls._column_id(tables, withs).as_('id'), Literal(0).as_('create_uid'), CurrentTimestamp().as_('create_date'), cls.write_uid.sql_cast(Literal(Null)).as_('write_uid'), cls.write_date.sql_cast(Literal(Null)).as_('write_date'), line.company.as_('company'), revenue.as_('revenue'), Count(line.order, distinct=True).as_('number'), ] @classmethod def _column_id(cls, tables, withs): line = tables['line'] return Min(line.id) @classmethod def _group_by(cls, tables, withs): line = tables['line'] return [line.company] @classmethod def _where(cls, tables, withs): pool = Pool() Location = pool.get('stock.location') context = Transaction().context line = tables['line'] where = Literal(True) from_date = context.get('from_date') if from_date: where &= line.date >= from_date to_date = context.get('to_date') if to_date: where &= line.date <= to_date warehouse = context.get('warehouse') if warehouse: locations = Location.search([ ('parent', 'child_of', warehouse), ], query=True) where &= line.location.in_(locations) return where @classmethod def _sale_states(cls): return ['confirmed', 'processing', 'done'] @property def time_series_all(self): delta = self._period_delta() for ts, next_ts in pairwise(self.time_series or []): yield ts if delta and next_ts: date = ts.date + delta while date < next_ts.date: yield None date += delta @classmethod def _period_delta(cls): context = Transaction().context return { 'year': relativedelta(years=1), 'month': relativedelta(months=1), 'day': relativedelta(days=1), }.get(context.get('period')) def get_trend(self, name): name = name[:-len('_trend')] if pygal: chart = pygal.Line() chart.add('', [ getattr(ts, name) if ts else 0 for ts in self.time_series_all ]) return chart.render_sparktext() def get_currency(self, name): return self.company.currency.id
class Abstract(ModelSQL, ModelView): company = fields.Many2One( 'company.company', lazy_gettext('stock.msg_stock_reporting_company')) cost = Monetary(lazy_gettext('stock.msg_stock_reporting_cost'), currency='currency', digits='currency') revenue = Monetary(lazy_gettext('stock.msg_stock_reporting_revenue'), currency='currency', digits='currency') profit = Monetary(lazy_gettext('stock.msg_stock_reporting_profit'), currency='currency', digits='currency') margin = fields.Numeric(lazy_gettext('stock.msg_stock_reporting_margin'), digits=(14, 4), states={ 'invisible': ~Eval('margin'), }) margin_trend = fields.Function( fields.Char(lazy_gettext('stock.msg_stock_reporting_margin_trend')), 'get_trend') time_series = None currency = fields.Many2One( 'currency.currency', lazy_gettext('stock.msg_stock_reporting_currency')) @classmethod def table_query(cls): from_item, tables, withs = cls._joins() return from_item.select(*cls._columns(tables, withs), where=cls._where(tables, withs), group_by=cls._group_by(tables, withs), with_=withs.values()) @classmethod def _joins(cls): pool = Pool() Company = pool.get('company.company') Currency = pool.get('currency.currency') Move = pool.get('stock.move') Location = pool.get('stock.location') tables = {} tables['move'] = move = Move.__table__() tables['move.company'] = company = Company.__table__() tables['move.company.currency'] = currency = Currency.__table__() tables['move.from_location'] = from_location = Location.__table__() tables['move.to_location'] = to_location = Location.__table__() withs = {} withs['currency_rate'] = currency_rate = With( query=Currency.currency_rate_sql()) withs['currency_rate_company'] = currency_rate_company = With( query=Currency.currency_rate_sql()) from_item = (move.join( currency_rate, condition=(move.currency == currency_rate.currency) & (currency_rate.start_date <= move.effective_date) & ((currency_rate.end_date == Null) | (currency_rate.end_date >= move.effective_date)) ).join(company, condition=move.company == company.id).join( currency, condition=company.currency == currency.id).join( currency_rate_company, condition=(company.currency == currency_rate_company.currency) & (currency_rate_company.start_date <= move.effective_date) & ((currency_rate_company.end_date == Null) | (currency_rate_company.end_date >= move.effective_date)) ).join(from_location, condition=(move.from_location == from_location.id)).join( to_location, condition=(move.to_location == to_location.id))) return from_item, tables, withs @classmethod def _columns(cls, tables, withs): move = tables['move'] from_location = tables['move.from_location'] to_location = tables['move.to_location'] currency = tables['move.company.currency'] sign = Case((from_location.type.in_(cls._to_location_types()) & to_location.type.in_(cls._from_location_types()), -1), else_=1) cost = cls._column_cost(tables, withs, sign) revenue = cls._column_revenue(tables, withs, sign) profit = revenue - cost margin = Case((revenue != 0, profit / revenue), else_=Null) return [ cls._column_id(tables, withs).as_('id'), Literal(0).as_('create_uid'), CurrentTimestamp().as_('create_date'), cls.write_uid.sql_cast(Literal(Null)).as_('write_uid'), cls.write_date.sql_cast(Literal(Null)).as_('write_date'), move.company.as_('company'), cls.cost.sql_cast(Round(cost, currency.digits)).as_('cost'), cls.revenue.sql_cast(Round(revenue, currency.digits)).as_('revenue'), cls.profit.sql_cast(Round(profit, currency.digits)).as_('profit'), cls.margin.sql_cast(Round(margin, cls.margin.digits[1])).as_('margin'), currency.id.as_('currency'), ] @classmethod def _column_id(cls, tables, withs): move = tables['move'] return Min(move.id) @classmethod def _column_cost(cls, tables, withs, sign): move = tables['move'] return Sum(sign * cls.cost.sql_cast(move.internal_quantity) * Coalesce(move.cost_price, 0)) @classmethod def _column_revenue(cls, tables, withs, sign): move = tables['move'] currency = withs['currency_rate'] currency_company = withs['currency_rate_company'] return Sum(sign * cls.revenue.sql_cast(move.quantity) * Coalesce(move.unit_price, 0) * Coalesce(currency_company.rate / currency.rate, 0)) @classmethod def _group_by(cls, tables, withs): move = tables['move'] currency = tables['move.company.currency'] return [move.company, currency.id, currency.digits] @classmethod def _where(cls, tables, withs): context = Transaction().context move = tables['move'] from_location = tables['move.from_location'] to_location = tables['move.to_location'] where = move.company == context.get('company') where &= ((from_location.type.in_(cls._from_location_types()) & to_location.type.in_(cls._to_location_types())) | (from_location.type.in_(cls._to_location_types()) & to_location.type.in_(cls._from_location_types()))) where &= move.state == 'done' from_date = context.get('from_date') if from_date: where &= move.effective_date >= from_date to_date = context.get('to_date') if to_date: where &= move.effective_date <= to_date return where @classmethod def _from_location_types(cls): return ['storage', 'drop'] @classmethod def _to_location_types(cls): types = ['customer'] if Transaction().context.get('include_lost'): types += ['lost_found'] return types @property def time_series_all(self): delta = self._period_delta() for ts, next_ts in pairwise(self.time_series or []): yield ts if delta and next_ts: date = ts.date + delta while date < next_ts.date: yield None date += delta @classmethod def _period_delta(cls): context = Transaction().context return { 'year': relativedelta(years=1), 'month': relativedelta(months=1), 'day': relativedelta(days=1), }.get(context.get('period')) def get_trend(self, name): name = name[:-len('_trend')] if pygal: chart = pygal.Line() chart.add('', [ getattr(ts, name) or 0 if ts else 0 for ts in self.time_series_all ]) return chart.render_sparktext() @classmethod def view_attributes(cls): return super().view_attributes() + [ ('/tree/field[@name="profit"]', 'visual', If(Eval('profit', 0) < 0, 'danger', '')), ('/tree/field[@name="margin"]', 'visual', If(Eval('margin', 0) < 0, 'danger', '')), ]
class Origin(origin_mixin(_states), ModelSQL, ModelView): "Account Statement Origin" __name__ = 'account.statement.origin' _rec_name = 'number' lines = fields.One2Many( 'account.statement.line', 'origin', "Lines", states={ 'readonly': ((Eval('statement_id', -1) < 0) | ~Eval('statement_state').in_(['draft', 'validated'])), }, domain=[ ('statement', '=', Eval('statement', -1)), ('date', '=', Eval('date', None)), ]) statement_id = fields.Function( fields.Integer("Statement ID"), 'on_change_with_statement_id') pending_amount = fields.Function(Monetary( "Pending Amount", currency='currency', digits='currency'), 'on_change_with_pending_amount', searcher='search_pending_amount') information = fields.Dict( 'account.statement.origin.information', "Information", readonly=True) @classmethod def __register__(cls, module_name): table = cls.__table_handler__(module_name) # Migration from 5.0: rename informations into information table.column_rename('informations', 'information') super(Origin, cls).__register__(module_name) @fields.depends('statement', '_parent_statement.id') def on_change_with_statement_id(self, name=None): if self.statement: return self.statement.id return -1 @fields.depends('lines', 'amount') def on_change_with_pending_amount(self, name=None): lines_amount = sum( getattr(l, 'amount') or Decimal(0) for l in self.lines) return (self.amount or Decimal(0)) - lines_amount @classmethod def search_pending_amount(cls, name, clause): pool = Pool() Line = pool.get('account.statement.line') table = cls.__table__() line = Line.__table__() _, operator, value = clause Operator = fields.SQL_OPERATORS[operator] query = (table.join(line, 'LEFT', condition=line.origin == table.id) .select(table.id, having=Operator( table.amount - Coalesce(Sum(line.amount), 0), value), group_by=table.id)) return [('id', 'in', query)] @classmethod def copy(cls, origins, default=None): default = default.copy() if default is not None else {} default.setdefault('lines') return super().copy(origins, default=default)
class ProductSupplierPrice(sequence_ordered(), ModelSQL, ModelView, MatchMixin): 'Product Supplier Price' __name__ = 'purchase.product_supplier.price' product_supplier = fields.Many2One('purchase.product_supplier', 'Supplier', required=True, ondelete='CASCADE') quantity = fields.Float("Quantity", required=True, domain=[('quantity', '>=', 0)], help='Minimal quantity.') unit_price = Monetary("Unit Price", currency='currency', required=True, digits=price_digits) uom = fields.Function( fields.Many2One('product.uom', 'UOM', help="The unit in which the quantity is specified."), 'on_change_with_uom') currency = fields.Function( fields.Many2One('currency.currency', 'Currency'), 'on_change_with_currency') @classmethod def __setup__(cls): super().__setup__() cls.__access__.add('product_supplier') @classmethod def __register__(cls, module_name): cursor = Transaction().connection.cursor() table = cls.__table_handler__(module_name) sql_table = cls.__table__() fill_sequence = not table.column_exist('sequence') super(ProductSupplierPrice, cls).__register__(module_name) # Migration from 3.2: replace quantity by sequence for order if fill_sequence: cursor.execute( *sql_table.update([sql_table.sequence], [sql_table.quantity])) @staticmethod def default_quantity(): return 0.0 @fields.depends('product_supplier', '_parent_product_supplier.product') def on_change_with_uom(self, name=None): if self.product_supplier and self.product_supplier.uom: return self.product_supplier.uom.id @fields.depends('product_supplier', '_parent_product_supplier.currency') def on_change_with_currency(self, name=None): if self.product_supplier and self.product_supplier.currency: return self.product_supplier.currency.id @staticmethod def get_pattern(): return {} def match(self, quantity, uom, pattern): pool = Pool() Uom = pool.get('product.uom') test_quantity = Uom.compute_qty(self.product_supplier.uom, self.quantity, uom) if test_quantity > abs(quantity): return False return super(ProductSupplierPrice, self).match(pattern)
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 Mixin: __slots__ = () statement = fields.Many2One( 'account.statement', "Statement", required=True, ondelete='CASCADE', states=_states) statement_state = fields.Function( fields.Selection('get_statement_states', "Statement State"), 'on_change_with_statement_state') company = fields.Function( fields.Many2One('company.company', "Company"), 'on_change_with_company', searcher='search_company') number = fields.Char("Number") date = fields.Date( "Date", required=True, states=_states) amount = Monetary( "Amount", currency='currency', digits='currency', required=True, states=_states) currency = fields.Function(fields.Many2One( 'currency.currency', "Currency"), 'on_change_with_currency') party = fields.Many2One( 'party.party', "Party", states=_states, context={ 'company': Eval('company', -1), }, depends={'company'}) account = fields.Many2One( 'account.account', "Account", domain=[ ('company', '=', Eval('company', 0)), ('type', '!=', None), ('closed', '!=', True), ], context={ 'date': Eval('date'), }, states=_states, depends={'date'}) description = fields.Char("Description", states=_states) @classmethod def __setup__(cls): super().__setup__() cls.__access__.add('statement') @classmethod def get_statement_states(cls): pool = Pool() Statement = pool.get('account.statement') return Statement.fields_get(['state'])['state']['selection'] @fields.depends('statement', '_parent_statement.state') def on_change_with_statement_state(self, name=None): if self.statement: return self.statement.state @fields.depends('statement', '_parent_statement.company') def on_change_with_company(self, name=None): if self.statement and self.statement.company: return self.statement.company.id @classmethod def search_company(cls, name, clause): return [('statement.' + clause[0],) + tuple(clause[1:])] @fields.depends('statement', '_parent_statement.journal') def on_change_with_currency(self, name=None): if self.statement and self.statement.journal: return self.statement.journal.currency.id
class Action(ModelSQL, ModelView): 'Customer Complaint Action' __name__ = 'sale.complaint.action' _states = { 'readonly': ((Eval('complaint_state') != 'draft') | Bool(Eval('result'))), } _line_states = { 'invisible': ~Eval('_parent_complaint', {}).get('origin_model', 'sale.line').in_( ['sale.line', 'account.invoice.line']), 'readonly': _states['readonly'], } complaint = fields.Many2One('sale.complaint', 'Complaint', required=True, ondelete='CASCADE', states=_states) action = fields.Selection([ ('sale_return', 'Create Sale Return'), ('credit_note', 'Create Credit Note'), ], 'Action', states=_states) sale_lines = fields.One2Many( 'sale.complaint.action-sale.line', 'action', "Sale Lines", states={ 'invisible': Eval('_parent_complaint', {}).get('origin_model', 'sale.sale') != 'sale.sale', 'readonly': _states['readonly'], }, help='Leave empty for all lines.') invoice_lines = fields.One2Many( 'sale.complaint.action-account.invoice.line', 'action', "Invoice Lines", states={ 'invisible': Eval('_parent_complaint', {}).get( 'origin_model', 'account.invoice.line') != 'account.invoice', 'readonly': _states['readonly'], }, help='Leave empty for all lines.') quantity = fields.Float("Quantity", digits='unit', states=_line_states, help='Leave empty for the same quantity.') unit = fields.Function( fields.Many2One('product.uom', 'Unit', states=_line_states), 'on_change_with_unit') unit_price = Monetary("Unit Price", currency='currency', digits=price_digits, states=_line_states, help='Leave empty for the same price.') amount = fields.Function(Monetary("Amount", 'currency', digits='currency'), 'on_change_with_amount') currency = fields.Function( fields.Many2One('currency.currency', "Currency"), 'on_change_with_currency') result = fields.Reference('Result', selection='get_result', readonly=True) complaint_state = fields.Function( fields.Selection('get_complaint_states', "Complaint State"), 'on_change_with_complaint_state') @classmethod def __setup__(cls): super().__setup__() cls.__access__.add('complaint') @fields.depends('complaint', '_parent_complaint.origin_model', '_parent_complaint.origin') def on_change_with_unit(self, name=None): if (self.complaint and self.complaint.origin_model in {'sale.line', 'account.invoice.line'}): return self.complaint.origin.unit.id @fields.depends('quantity', 'unit_price', 'currency', 'sale_lines', 'invoice_lines', 'complaint', '_parent_complaint.origin_model', '_parent_complaint.origin') def on_change_with_amount(self, name=None): if self.complaint: if self.complaint.origin_model in { 'sale.line', 'account.invoice.line' }: if self.quantity is not None: quantity = self.quantity else: quantity = self.complaint.origin.quantity if self.unit_price is not None: unit_price = self.unit_price else: unit_price = self.complaint.origin.unit_price amount = Decimal(str(quantity)) * unit_price if self.currency: amount = self.currency.round(amount) return amount elif self.complaint.origin_model == 'sale.sale': if not self.sale_lines: if self.complaint and self.complaint.origin: return self.complaint.origin.untaxed_amount else: return sum( getattr(l, 'amount', None) or Decimal(0) for l in self.sale_lines) elif self.complaint.origin_model == 'account.invoice': if not self.invoice_lines: if self.complaint and self.complaint.origin: return self.complaint.origin.untaxed_amount else: return sum( getattr(l, 'amount', None) or Decimal(0) for l in self.invoice_lines) @fields.depends('complaint', '_parent_complaint.origin_model', '_parent_complaint.origin') def on_change_with_currency(self, name=None): if (self.complaint and self.complaint.origin_model in { 'sale.sale', 'sale.line', 'account.invoice', 'account.invoice.line' }): return self.complaint.origin.currency.id @classmethod def get_complaint_states(cls): pool = Pool() Complaint = pool.get('sale.complaint') return Complaint.fields_get(['state'])['state']['selection'] @fields.depends('complaint', '_parent_complaint.state') def on_change_with_complaint_state(self, name=None): if self.complaint: return self.complaint.state @classmethod def _get_result(cls): 'Return list of Model names for result Reference' return ['sale.sale', 'account.invoice'] @classmethod def get_result(cls): pool = Pool() Model = pool.get('ir.model') get_name = Model.get_name models = cls._get_result() return [(None, '')] + [(m, get_name(m)) for m in models] @classmethod def copy(cls, actions, default=None): if default is None: default = {} else: default = default.copy() default.setdefault('result', None) return super().copy(actions, default=default) def do(self): return getattr(self, 'do_%s' % self.action)() def do_sale_return(self): pool = Pool() Sale = pool.get('sale.sale') Line = pool.get('sale.line') if isinstance(self.complaint.origin, (Sale, Line)): default = {} if isinstance(self.complaint.origin, Sale): sale = self.complaint.origin if self.sale_lines: sale_lines = [l.line for l in self.sale_lines] line2qty = { l.line.id: l.get_quantity() for l in self.sale_lines } line2price = { l.line.id: l.get_unit_price() for l in self.sale_lines } default['quantity'] = lambda o: line2qty.get(o['id']) default['unit_price'] = lambda o: line2price.get(o['id']) else: sale_lines = [l for l in sale.lines if l.type == 'line'] elif isinstance(self.complaint.origin, Line): sale_line = self.complaint.origin sale = sale_line.sale sale_lines = [sale_line] if self.quantity is not None: default['quantity'] = self.quantity if self.unit_price is not None: default['unit_price'] = self.unit_price return_sale, = Sale.copy([sale], default={'lines': None}) default['sale'] = return_sale.id Line.copy(sale_lines, default=default) else: return return_sale.origin = self.complaint for line in return_sale.lines: if line.type == 'line': line.quantity *= -1 return_sale.lines = return_sale.lines # Force saving return return_sale def do_credit_note(self): pool = Pool() Invoice = pool.get('account.invoice') Line = pool.get('account.invoice.line') if isinstance(self.complaint.origin, (Invoice, Line)): line2qty = line2price = {} if isinstance(self.complaint.origin, Invoice): invoice = self.complaint.origin if self.invoice_lines: invoice_lines = [l.line for l in self.invoice_lines] line2qty = { l.line: l.quantity for l in self.invoice_lines if l.quantity is not None } line2price = { l.line: l.unit_price for l in self.invoice_lines if l.unit_price is not None } else: invoice_lines = [ l for l in invoice.lines if l.type == 'line' ] elif isinstance(self.complaint.origin, Line): invoice_line = self.complaint.origin invoice = invoice_line.invoice invoice_lines = [invoice_line] if self.quantity is not None: line2qty = {invoice_line: self.quantity} if self.unit_price is not None: line2price = {invoice_line: self.unit_price} with Transaction().set_context(_account_invoice_correction=True): credit_note, = Invoice.copy([invoice], default={ 'lines': [], 'taxes': [], }) # Copy each line one by one to get negative and positive lines # following each other for invoice_line in invoice_lines: qty = line2qty.get(invoice_line, invoice_line.quantity) unit_price = invoice_line.unit_price - line2price.get( invoice_line, invoice_line.unit_price) Line.copy( [invoice_line], default={ 'invoice': credit_note.id, 'quantity': -qty, 'origin': str(self.complaint), }) credit_line, = Line.copy( [invoice_line], default={ 'invoice': credit_note.id, 'quantity': qty, 'unit_price': unit_price, 'origin': str(self.complaint), }) credit_note.update_taxes() else: return return credit_note @classmethod def delete(cls, actions): for action in actions: if action.result: raise AccessError( gettext('sale_complaint.msg_action_delete_result', action=action.rec_name)) super(Action, cls).delete(actions)
class Party(metaclass=PoolMeta): __name__ = 'party.party' deposit = fields.Function(Monetary("Deposit", currency='currency', digits='currency'), 'get_deposit', searcher='search_deposit') @classmethod def get_deposit(cls, parties, name): pool = Pool() MoveLine = pool.get('account.move.line') Account = pool.get('account.account') AccountType = pool.get('account.account.type') User = pool.get('res.user') cursor = Transaction().connection.cursor() line = MoveLine.__table__() account = Account.__table__() account_type = AccountType.__table__() values = {p.id: Decimal(0) for p in parties} user = User(Transaction().user) if not user.company: return values currency = user.company.currency line_clause, _ = MoveLine.query_get(line) for sub_parties in grouped_slice(parties): party_clause = reduce_ids(line.party, [p.id for p in sub_parties]) cursor.execute( *line.join(account, condition=account.id == line.account).join( account_type, condition=account.type == account_type.id). select( line.party, # Use credit - debit to positive deposit amount Sum(Coalesce(line.credit, 0) - Coalesce(line.debit, 0)), where=account_type.deposit & party_clause & (line.reconciliation == Null) & (account.company == user.company.id) & line_clause, group_by=line.party)) for party_id, value in cursor: # SQLite uses float for SUM if not isinstance(value, Decimal): value = currency.round(Decimal(str(value))) values[party_id] = value return values @classmethod def search_deposit(cls, name, clause): pool = Pool() MoveLine = pool.get('account.move.line') Account = pool.get('account.account') AccountType = pool.get('account.account.type') User = pool.get('res.user') line = MoveLine.__table__() account = Account.__table__() account_type = AccountType.__table__() user = User(Transaction().user) if not user.company: return [] line_clause, _ = MoveLine.query_get(line) Operator = fields.SQL_OPERATORS[clause[1]] query = (line.join(account, condition=account.id == line.account).join( account_type, condition=account.type == account_type.id).select( line.party, where=account.active & account_type.deposit & (line.party != Null) & (line.reconciliation == Null) & (account.company == user.company.id) & line_clause, group_by=line.party, having=Operator( Sum(Coalesce(line.debit, 0) - Coalesce(line.credit, 0)), Decimal(clause[2] or 0)))) return [('id', 'in', query)] def get_deposit_balance(self, deposit_account): 'Return the deposit account balance (debit - credit) for the party' pool = Pool() MoveLine = pool.get('account.move.line') transaction = Transaction() cursor = transaction.connection.cursor() line = MoveLine.__table__() assert deposit_account.type.deposit where = ((line.account == deposit_account.id) & (line.party == self.id) & (line.reconciliation == Null)) if transaction.database.has_select_for(): cursor.execute(*line.select( Literal(1), where=where, for_=For('UPDATE', nowait=True))) else: MoveLine.lock() cursor.execute(*line.select(Sum( Coalesce(line.debit, 0) - Coalesce(line.credit, 0)), where=where)) amount, = cursor.fetchone() if amount and not isinstance(amount, Decimal): currency = deposit_account.company.currency amount = currency.round(Decimal(str(amount))) return amount or Decimal(0) def check_deposit(self, deposit_account, sign=1): '''Check if the deposit account balance (debit - credit) has the same sign for the party''' assert sign in (1, -1) amount = self.get_deposit_balance(deposit_account) return not amount or ((amount < 0) == (sign < 0))
class SaleOpportunityReportMixin: __slots__ = () number = fields.Integer('Number') converted = fields.Integer('Converted') conversion_rate = fields.Function( fields.Float('Conversion Rate', digits=(1, 4)), 'get_conversion_rate') won = fields.Integer('Won') winning_rate = fields.Function(fields.Float('Winning Rate', digits=(1, 4)), 'get_winning_rate') lost = fields.Integer('Lost') company = fields.Many2One('company.company', 'Company') currency = fields.Function( fields.Many2One('currency.currency', 'Currency'), 'get_currency') amount = Monetary("Amount", currency='currency', digits='currency') converted_amount = Monetary("Converted Amount", currency='currency', digits='currency') conversion_amount_rate = fields.Function( fields.Float('Conversion Amount Rate', digits=(1, 4)), 'get_conversion_amount_rate') won_amount = Monetary("Won Amount", currency='currency', digits='currency') winning_amount_rate = fields.Function( fields.Float('Winning Amount Rate', digits=(1, 4)), 'get_winning_amount_rate') @staticmethod def _converted_state(): return ['converted', 'won'] @staticmethod def _won_state(): return ['won'] @staticmethod def _lost_state(): return ['lost'] def get_conversion_rate(self, name): if self.number: digits = getattr(self.__class__, name).digits[1] return round(float(self.converted) / self.number, digits) else: return 0.0 def get_winning_rate(self, name): if self.number: digits = getattr(self.__class__, name).digits[1] return round(float(self.won) / self.number, digits) else: return 0.0 def get_currency(self, name): return self.company.currency.id def get_conversion_amount_rate(self, name): if self.amount: digits = getattr(self.__class__, name).digits[1] return round( float(self.converted_amount) / float(self.amount), digits) else: return 0.0 def get_winning_amount_rate(self, name): if self.amount: digits = getattr(self.__class__, name).digits[1] return round(float(self.won_amount) / float(self.amount), digits) else: return 0.0 @classmethod def table_query(cls): Opportunity = Pool().get('sale.opportunity') opportunity = Opportunity.__table__() return opportunity.select( Max(opportunity.create_uid).as_('create_uid'), Max(opportunity.create_date).as_('create_date'), Max(opportunity.write_uid).as_('write_uid'), Max(opportunity.write_date).as_('write_date'), opportunity.company, Count(Literal(1)).as_('number'), Sum( Case((opportunity.state.in_( cls._converted_state()), Literal(1)), else_=Literal(0))).as_('converted'), Sum( Case((opportunity.state.in_(cls._won_state()), Literal(1)), else_=Literal(0))).as_('won'), Sum( Case((opportunity.state.in_(cls._lost_state()), Literal(1)), else_=Literal(0))).as_('lost'), Sum(opportunity.amount).as_('amount'), Sum( Case((opportunity.state.in_( cls._converted_state()), opportunity.amount), else_=Literal(0))).as_('converted_amount'), Sum( Case((opportunity.state.in_( cls._won_state()), opportunity.amount), else_=Literal(0))).as_('won_amount'))