class Statement(dd.Model): class Meta: app_label = 'b2c' abstract = dd.is_abstract_model(__name__, 'Statement') verbose_name = _("Statement") verbose_name_plural = _("Statements") def __str__(self): return self.statement_number account = dd.ForeignKey('b2c.Account') statement_number = models.CharField( _('Statement number'), blank=False, max_length=10, help_text=_("Combination of the year and the legal sequential number" " of the paper statement.")) start_date = models.DateField(_('Start date'), null=True) end_date = models.DateField(_('End date'), null=True) # date_done = models.DateTimeField(_('Import Date'), null=True) balance_start = dd.PriceField(_("Initial amount"), null=True) balance_end = dd.PriceField(_("Final amount"), null=True) # balance_end_real = dd.PriceField(_("Real end balance"), null=True) local_currency = models.CharField(_('Currency'), max_length=3)
class BankStatement(DatedFinancialVoucher): class Meta: app_label = 'finan' abstract = dd.is_abstract_model(__name__, 'BankStatement') verbose_name = _("Bank Statement") verbose_name_plural = _("Bank Statements") balance1 = dd.PriceField(_("Old balance"), default=ZERO) balance2 = dd.PriceField(_("New balance"), default=ZERO, blank=True) # show_items = dd.ShowSlaveTable('finan.ItemsByBankStatement') def get_previous_voucher(self): if not self.journal_id: #~ logger.info("20131005 no journal") return None qs = self.__class__.objects.filter( journal=self.journal).order_by('-entry_date') if qs.count() > 0: #~ logger.info("20131005 no other vouchers") return qs[0] def on_create(self, ar): super(BankStatement, self).on_create(ar) if self.balance1 == ZERO: prev = self.get_previous_voucher() if prev is not None: #~ logger.info("20131005 prev is %s",prev) self.balance1 = prev.balance2 def on_duplicate(self, ar, master): self.balance1 = self.balance2 = ZERO super(BankStatement, self).on_duplicate(ar, master) def get_wanted_movements(self): """Implements :meth:`lino_xl.lib.ledger.Voucher.get_wanted_movements` for bank statements. As a side effect this also computes the :attr:`balance1` and :attr:`balance2` fields and saves the voucher. """ # dd.logger.info("20151211 cosi.BankStatement.get_wanted_movements()") a = self.journal.account if not a: warn_jnl_account(self.journal) amount, movements_and_items = self.get_finan_movements() self.balance2 = self.balance1 + amount if abs(self.balance2) > MAX_AMOUNT: dd.logger.warning("Oops, %s is too big", self.balance2) return for m, i in movements_and_items: yield m yield self.create_movement( None, (a, None), None, self.journal.dc, amount) # side effect!: self.full_clean() self.save()
def addann(kw, name, dc, flt): mvts = rt.models.ledger.Movement.objects.filter(dc=dc, **flt) mvts = mvts.order_by() mvts = mvts.values(outer_link) # this was the important thing mvts = mvts.annotate( total=Sum('amount', output_field=dd.PriceField())) mvts = mvts.values('total') kw[name] = Subquery(mvts, output_field=dd.PriceField())
def addann(self, kw, name, dc, flt): mvts = rt.models.ledger.Movement.objects.filter(dc=dc, **flt) mvts = mvts.order_by() mvts = mvts.values(self.outer_link) # this was the important thing mvts = mvts.annotate(total=Sum( 'amount', output_field=dd.PriceField(decimal_places=14)))# For Django2 we need to set decimal_places to 14 which is the max of decimal_places used in ReportEntry fields. mvts = mvts.values('total') kw[name] = Subquery(mvts, output_field=dd.PriceField(decimal_places=14))
class BankStatement(DatedFinancialVoucher): """A **bank statement** is a document issued by the bank, which reports all transactions which occured on a given account during a given period. .. attribute:: balance1 The old (or start) balance. .. attribute:: balance2 The new (or end) balance. """ class Meta: app_label = 'finan' abstract = dd.is_abstract_model(__name__, 'BankStatement') verbose_name = _("Bank Statement") verbose_name_plural = _("Bank Statements") balance1 = dd.PriceField(_("Old balance"), default=ZERO) balance2 = dd.PriceField(_("New balance"), default=ZERO, blank=True) def get_previous_voucher(self): if not self.journal_id: #~ logger.info("20131005 no journal") return None qs = self.__class__.objects.filter( journal=self.journal).order_by('-voucher_date') if qs.count() > 0: #~ logger.info("20131005 no other vouchers") return qs[0] def on_create(self, ar): super(BankStatement, self).on_create(ar) if self.balance1 == ZERO: prev = self.get_previous_voucher() if prev is not None: #~ logger.info("20131005 prev is %s",prev) self.balance1 = prev.balance2 def get_wanted_movements(self): # dd.logger.info("20151211 cosi.BankStatement.get_wanted_movements()") a = self.journal.account if not a: warn_jnl_account(self.journal) amount, movements_and_items = self.get_finan_movements() self.balance2 = self.balance1 + amount for m, i in movements_and_items: yield m yield self.create_movement(None, a, None, self.journal.dc, amount)
class QtyVatItemBase(VatItemBase): """Model mixin for items of a :class:`VatTotal`, adds `unit_price` and `qty`. Abstract Base class for :class:`lino_cosi.lib.sales.InvoiceItem` and :class:`lino_cosi.lib.sales.OrderItem`, i.e. the lines of invoices *with* unit prices and quantities. """ class Meta: abstract = True unit_price = dd.PriceField(_("Unit price"), blank=True, null=True) qty = dd.QuantityField(_("Quantity"), blank=True, null=True) def unit_price_changed(self, ar): self.reset_totals(ar) def qty_changed(self, ar): self.reset_totals(ar) def reset_totals(self, ar): super(QtyVatItemBase, self).reset_totals(ar) # if not self.voucher.auto_compute_totals: # if self.qty: # if self.voucher.item_vat: # self.unit_price = self.total_incl / self.qty # else: # self.unit_price = self.total_base / self.qty if self.unit_price is not None and self.qty is not None: self.set_amount(ar, myround(self.unit_price * self.qty))
def inject_tradetype_fields(sender, **kw): """This defines certain database fields related to your :class:`TradeTypes`. """ # print(20200622, list([i.invoice_account_field_name for i in TradeTypes.items()])) for tt in TradeTypes.items(): if tt.invoice_account_field_name is not None: dd.inject_field( 'contacts.Partner', tt.invoice_account_field_name, dd.ForeignKey('ledger.Account', verbose_name=tt.invoice_account_field_label, on_delete=models.PROTECT, related_name='partners_by_' + tt.invoice_account_field_name, blank=True, null=True)) if tt.base_account_field_name is not None: dd.inject_field( 'products.Product', tt.base_account_field_name, dd.ForeignKey('ledger.Account', verbose_name=tt.base_account_field_label, on_delete=models.PROTECT, related_name='products_by_' + tt.base_account_field_name, blank=True, null=True)) if tt.price_field_name is not None: dd.inject_field( 'products.Product', tt.price_field_name, dd.PriceField(verbose_name=tt.price_field_label, blank=True, null=True))
class Product(mixins.BabelNamed): price = dd.PriceField(_("Price"), blank=True, null=True) category = Categories.field(blank=True, null=True) class Meta: verbose_name = 'Product' verbose_name_plural = 'Products'
class InvoiceItem(AccountVoucherItem, ProjectRelated): """An item of an :class:`AccountInvoice`.""" class Meta: verbose_name = _("Invoice item") verbose_name_plural = _("Invoice items") voucher = dd.ForeignKey('vatless.AccountInvoice', related_name='items') title = models.CharField(_("Description"), max_length=200, blank=True) amount = dd.PriceField(_("Amount"), blank=True, null=True)
class VatProductInvoice(SalesDocument, Matching): class Meta: app_label = 'sales' abstract = dd.is_abstract_model(__name__, 'VatProductInvoice') verbose_name = _("Sales invoice") verbose_name_plural = _("Sales invoices") quick_search_fields = "partner__name subject" # show_items = dd.ShowSlaveTable('sales.ItemsByInvoice') # make_copy = MakeCopy() @classmethod def get_registrable_fields(cls, site): for f in super(VatProductInvoice, cls).get_registrable_fields(site): yield f yield 'due_date' # yield 'order' yield 'voucher_date' yield 'entry_date' yield 'user' # yield 'item_vat' def get_print_items(self, ar): return self.print_items_table.request(self) @dd.virtualfield(dd.PriceField(_("Balance to pay"))) def balance_to_pay(self, ar): Movement = rt.models.ledger.Movement qs = Movement.objects.filter(partner=self.get_partner(), cleared=False, match=self.get_match()) return Movement.get_balance(not self.journal.dc, qs) @dd.virtualfield(dd.PriceField(_("Balance before"))) def balance_before(self, ar): Movement = rt.models.ledger.Movement qs = Movement.objects.filter(partner=self.get_partner(), cleared=False, value_date__lte=self.entry_date) qs = qs.exclude(voucher=self) return Movement.get_balance(not self.journal.dc, qs)
class ReportEntry(dd.Model): class Meta: abstract = True show_in_site_search = False allow_cascaded_delete = ['report'] report = dd.ForeignKey('sheets.Report') old_d = dd.PriceField(_("Debit before"), 14, null=True, blank=True) old_c = dd.PriceField(_("Credit before"), 14, null=True, blank=True) during_d = dd.PriceField(_("Debit"), 14, null=True, blank=True) during_c = dd.PriceField(_("Credit"), 14, null=True, blank=True) def new_balance(self): return Balance(self.old_d, self.old_c) + Balance( self.during_d, self.during_c) def value2html(self, ar): txt = dd.format_currency(self.value, False, True) if self.item.is_heading(): # return E.b(txt) return E.div(E.b(txt), align="right") # return txt return E.div(txt, align="right") @dd.virtualfield(dd.PriceField(_("Debit after"))) def new_d(self, ar): return self.new_balance().d @dd.virtualfield(dd.PriceField(_("Credit after"))) def new_c(self, ar): return self.new_balance().c
class Statement(dd.Model): """A bank statement. This data is automaticaly imported by :class:`ImportStatements`. .. attribute:: sequence_number The legal sequential number of the statement, as assigned by the bank. See `LegalSequenceNumber <https://www.iso20022.org/standardsrepository/public/wqt/Content/mx/camt.053.001.02#mx/camt.053.001.02/Statement/LegalSequenceNumber>`_ (`<LglSeqNb>`) for details. """ class Meta: app_label = 'b2c' abstract = dd.is_abstract_model(__name__, 'Statement') verbose_name = _("Statement") verbose_name_plural = _("Statements") def __str__(self): return self.statement_number account = dd.ForeignKey('b2c.Account') statement_number = models.CharField( _('Statement number'), blank=False, max_length=10, help_text=_("Combination of the year and the legal sequential number" " of the paper statement.")) start_date = models.DateField(_('Start date'), null=True) end_date = models.DateField(_('End date'), null=True) # date_done = models.DateTimeField(_('Import Date'), null=True) balance_start = dd.PriceField(_("Initial amount"), null=True) balance_end = dd.PriceField(_("Final amount"), null=True) # balance_end_real = dd.PriceField(_("Real end balance"), null=True) local_currency = models.CharField(_('Currency'), max_length=3)
class PaymentOrder(FinancialVoucher): """A **payment order** is when a user instructs a bank to execute a series of outgoing transactions from a given bank account. """ class Meta: app_label = 'finan' abstract = dd.is_abstract_model(__name__, 'PaymentOrder') verbose_name = _("Payment Order") verbose_name_plural = _("Payment Orders") total = dd.PriceField(_("Total"), blank=True, null=True) execution_date = models.DateField(_("Execution date"), blank=True, null=True) def get_wanted_movements(self): """Implements :meth:`lino_cosi.lib.ledger.models.Voucher.get_wanted_movements` for payment orders. The generated movements """ # dd.logger.info("20151211 cosi.PaymentOrder.get_wanted_movements()") acc = self.journal.account if not acc: warn_jnl_account(self.journal) amount, movements_and_items = self.get_finan_movements() self.total = -amount for m, i in movements_and_items: yield m if acc.needs_partner: yield self.create_movement(i, acc, m.project, not m.dc, m.amount, partner=m.partner, match=m.match or m.get_default_match()) if not acc.needs_partner: yield self.create_movement(None, acc, None, not self.journal.dc, amount) def add_item_from_due(self, obj, **kwargs): # if obj.bank_account is None: # return i = super(PaymentOrder, self).add_item_from_due(obj, **kwargs) i.bank_account = obj.bank_account return i
class ShippingMode(mixins.BabelNamed): """ Represents a possible method of how the items described in a :class:`SalesDocument` are to be transferred from us to our customer. .. attribute:: price """ class Meta: app_label = 'delivery' verbose_name = _("Shipping Mode") verbose_name_plural = _("Shipping Modes") price = dd.PriceField(blank=True, null=True)
class Product(dd.Model): product_name = models.CharField("Product ", max_length=25,null=True , blank=True) product_type = models.CharField("Product Type", max_length=25, null=True, blank=True) #product_price= models.DecimalField("Price", decimal_places=2, max_digits=10) #product_qt=models.IntegerField("Quantity") product_qt = dd.QuantityField()#modified with Hand only last_qt = dd.QuantityField(default='0') rest_qt = dd.QuantityField() #new_qt = dd.QuantityField() new_qt = models.BooleanField(default=False) product_price = dd.PriceField() def __str__(self): #not work self.product_qt = self.product_qt - Delivery.qty return "%s ,%s EUR |*%s EUR*" % (self.product_name, self.product_price, self.product_qt * self.product_price)
def inject_tradetype_fields(sender, **kw): """This defines certain database fields related to your :class:`TradeTypes`. """ for tt in TradeTypes.items(): if tt.partner_account_field_name is not None: dd.inject_field( 'system.SiteConfig', tt.partner_account_field_name, dd.ForeignKey('accounts.Account', verbose_name=tt.partner_account_field_label, related_name='configs_by_' + tt.partner_account_field_name, blank=True, null=True)) if tt.vat_account_field_name is not None: dd.inject_field( 'system.SiteConfig', tt.vat_account_field_name, dd.ForeignKey('accounts.Account', verbose_name=tt.vat_account_field_label, related_name='configs_by_' + tt.vat_account_field_name, blank=True, null=True)) if tt.base_account_field_name is not None: dd.inject_field( 'system.SiteConfig', tt.base_account_field_name, dd.ForeignKey('accounts.Account', verbose_name=tt.base_account_field_label, related_name='configs_by_' + tt.base_account_field_name, blank=True, null=True)) dd.inject_field( 'products.Product', tt.base_account_field_name, dd.ForeignKey('accounts.Account', verbose_name=tt.base_account_field_label, related_name='products_by_' + tt.base_account_field_name, blank=True, null=True)) if tt.price_field_name is not None: dd.inject_field( 'products.Product', tt.price_field_name, dd.PriceField(verbose_name=tt.price_field_label, blank=True, null=True))
class Item(dd.Model): name = models.CharField(max_length=50) qty = dd.QuantityField(default='1') discount = dd.QuantityField() price = dd.PriceField() def __unicode__(self): if self.discount is None: return "%s x %s at %s = %s EUR" % (self.qty, self.name, self.price, self.total()) else: return "%s x %s at %s (-%s) = %s EUR" % ( self.qty, self.name, self.price, self.discount, self.total()) def total(self): if self.discount is None: return self.qty * self.price else: return self.qty * (1 - self.discount) * self.price
class FinancialVoucherItem(VoucherItem, SequencedVoucherItem, ProjectRelated, Matching): class Meta: abstract = True verbose_name = _("Item") verbose_name_plural = _("Items") app_label = "finan" amount = dd.PriceField(_("Amount"), default=ZERO, null=False) dc = DebitOrCreditField() remark = models.CharField( _("Your reference"), max_length=200, blank=True) account = dd.ForeignKey('ledger.Account', blank=True, null=True) partner = dd.ForeignKey('contacts.Partner', blank=True, null=True) quick_search_fields = 'remark account__ref account__name partner__name' @dd.chooser(simple_values=True) def match_choices(cls, voucher, partner): return cls.get_match_choices(voucher.journal, partner) def add_item_from_due(self, obj, **kwargs): return self.voucher.add_item_from_due(obj, **kwargs) # def get_default_match(self): # 20191226 def __str__(self): """Used as `match` when no explicit match is specified for this movement. """ if self.voucher_id and self.voucher.journal_id: return "%s %s:%s" % ( self.voucher.journal.ref, self.voucher.number, self.seqno) # return str(self.date) return models.Model.__str__(self) # return super(FinancialVoucherItem, self).__str__() def get_siblings(self): return self.voucher.items.all() def match_changed(self, ar): if not self.match or not self.voucher.journal.auto_fill_suggestions: return flt = dict(match=self.match, cleared=False) if not dd.plugins.finan.suggest_future_vouchers: flt.update(value_date__lte=self.voucher.entry_date) self.collect_suggestions(ar, flt) def partner_changed(self, ar): """The :meth:`trigger method <lino.core.model.Model.FOO_changed>` for :attr:`partner`. """ # dd.logger.info("20160329 FinancialMixin.partner_changed") if not self.partner or not self.voucher.journal.auto_fill_suggestions: return flt = dict(partner=self.partner, cleared=False) if not dd.plugins.finan.suggest_future_vouchers: flt.update(value_date__lte=self.voucher.entry_date) if self.match: flt.update(match=self.match) self.collect_suggestions(ar, models.Q(**flt)) def collect_suggestions(self, ar, flt): suggestions = list(ledger.get_due_movements( self.voucher.journal.dc, flt)) if len(suggestions) == 0: self.match = "" elif len(suggestions) == 1: self.fill_suggestion(suggestions[0]) elif ar: self.match = _("{} suggestions").format(len(suggestions)) # def ok(ar2): # # self.fill_suggestion(suggestions[0]) # # self.set_grouper(suggestions) # ar2.error(_("Oops, not implemented.")) # return # elems = ["Cool! ", E.b(str(self.partner)), # " has ", E.b(str(len(suggestions))), # " suggestions! Click "] # ba = ar.actor.get_action_by_name('suggest') # elems.append(ar.action_button(ba, self)) # elems.append(".") # html = E.p(*elems) # # dd.logger.info("20160526 %s", E.tostring(html)) # ar.success(E.tostring(html), alert=True) # # ar.confirm(ok, E.tostring(html)) def account_changed(self, ar): if not self.account: return if self.account.default_amount: self.amount = self.account.default_amount self.dc = not self.voucher.journal.dc # self.dc = not self.account.type.dc return if self.voucher.auto_compute_amount: total = Decimal() dc = self.voucher.journal.dc for item in self.voucher.items.exclude(id=self.id): if item.dc == dc: total -= item.amount else: total += item.amount if total < 0: dc = not dc total = - total self.dc = dc self.amount = total def get_partner(self): return self.partner or self.project def fill_suggestion(self, match): """Fill the fields of this item from the given suggestion (a `DueMovement` instance). """ # if not match.balance: # raise Exception("20151117") if match.trade_type: self.account = match.trade_type.get_main_account() if self.account_id is None: self.account = match.account self.dc = match.dc self.amount = - match.balance self.match = match.match self.project = match.project self.partner = match.partner # when called from match_changed() def guess_amount(self): self.account_changed(None) if self.amount is not None: return self.partner_changed(None) if self.amount is not None: return self.amount = ZERO def full_clean(self, *args, **kwargs): # if self.amount is None: # if self.account and self.account.default_amount: # self.amount = self.account.default_amount # self.dc = self.account.type.dc # else: # self.amount = ZERO if self.dc is None: self.dc = not self.voucher.journal.dc # if self.account is None: # self.dc = not self.voucher.journal.dc # else: # self.dc = not self.account.type.dc if self.amount is None: # amount can be None e.g. if user entered '' self.guess_amount() elif self.amount < 0: self.amount = - self.amount self.dc = not self.dc if False: # temporarily deactivated for data migration problems = list(FinancialVoucherItemChecker.check_instance(self)) if len(problems): raise ValidationError("20181120 {}".format( '\n'.join([p[1] for p in problems]))) # dd.logger.info("20151117 FinancialVoucherItem.full_clean a %s", self.amount) super(FinancialVoucherItem, self).full_clean(*args, **kwargs)
class Item(dd.Model): class Meta: app_label = 'invoicing' abstract = dd.is_abstract_model(__name__, 'Item') verbose_name = _("Invoicing suggestion") verbose_name_plural = _("Invoicing suggestions") plan = dd.ForeignKey('invoicing.Plan', related_name="items") partner = dd.ForeignKey('contacts.Partner') generator_type = dd.ForeignKey(ContentType, editable=True, blank=True, null=True, verbose_name=format_lazy( u"{} {}", generator_label, _('(type)'))) generator_id = GenericForeignKeyIdField(generator_type, editable=True, blank=True, null=True, verbose_name=format_lazy( u"{} {}", generator_label, _('(object)'))) generator = GenericForeignKey('generator_type', 'generator_id', verbose_name=generator_label) # first_date = models.DateField(_("First date")) # last_date = models.DateField(_("Last date")) amount = dd.PriceField(_("Amount"), default=ZERO) # number_of_invoiceables = models.IntegerField(_("Number"), default=0) preview = models.TextField(_("Preview"), blank=True) selected = models.BooleanField(_("Selected"), default=True) invoice = dd.ForeignKey(dd.plugins.invoicing.voucher_model, verbose_name=_("Invoice"), null=True, blank=True, on_delete=models.SET_NULL) exec_item = ExecuteItem() @dd.displayfield(_("Invoice")) def invoice_button(self, ar): if ar is not None: if self.invoice_id: return self.invoice.obj2href(ar) ba = ar.actor.get_action_by_name('exec_item') if ar.actor.get_row_permission(self, ar, None, ba): return ar.action_button(ba, self) return '' def create_invoice(self, ar): if self.plan.area_id is None: raise Warning(_("No area specified")) if self.plan.area.journal is None: raise Warning( _("No journal configured for {}").format(self.plan.area)) invoice = self.plan.create_invoice(partner=self.partner, user=ar.get_user()) lng = invoice.get_print_language() items = [] max_date = self.plan.get_max_date() with translation.override(lng): if self.generator: generators = [self.generator] else: generators = [ ig for ig in self.plan.get_generators_for_plan(self.partner) if ig.allow_group_invoices() ] for ig in generators: info = ig.compute_invoicing_info(max_date) pt = ig.get_invoiceable_payment_term() if pt: invoice.payment_term = pt pt = ig.get_invoiceable_paper_type() if pt: invoice.paper_type = pt # for i in ig.get_invoice_items(info, ITEM_MODEL, ar): for i in ig.get_invoice_items(info, invoice, ar): # kwargs.update(voucher=invoice) # i = ITEM_MODEL(**kwargs) # if 'amount' in kwargs: # i.set_amount(ar, kwargs['amount']) # amount = kwargs.get('amount', ZERO) # if amount: # i.set_amount(ar, amount) items.append((ig, i)) if len(items) == 0: # neither invoice nor items are saved raise Warning(_("No invoiceables found for %s.") % self) # dd.logger.warning( # _("No invoiceables found for %s.") % self.partner) # return invoice.full_clean() invoice.save() for ig, i in items: # assign voucher after it has been saved i.voucher = invoice ig.setup_invoice_item(i) # if not i.title: # i.title = ig.get_invoiceable_title(invoice) # compute the sales_price and amounts, but don't change # title and description # title = i.title # i.product_changed() i.discount_changed() # i.title = title i.full_clean() i.save() self.invoice = invoice self.full_clean() self.save() invoice.compute_totals() invoice.full_clean() invoice.save() invoice.register(ar) return invoice def __str__(self): return "{0} {1}".format(self.plan, self.partner)
class PaymentOrder(FinancialVoucher, Printable): class Meta: app_label = 'finan' abstract = dd.is_abstract_model(__name__, 'PaymentOrder') verbose_name = _("Payment Order") verbose_name_plural = _("Payment Orders") total = dd.PriceField(_("Total"), blank=True, null=True) execution_date = models.DateField(_("Execution date"), blank=True, null=True) # show_items = dd.ShowSlaveTable('finan.ItemsByPaymentOrder') write_xml = WritePaymentsInitiation() @dd.displayfield(_("Print")) def print_actions(self, ar): if ar is None: return '' elems = [] elems.append(ar.instance_action_button(self.write_xml)) return E.p(*join_elems(elems, sep=", ")) def get_wanted_movements(self): """Implements :meth:`lino_xl.lib.ledger.Voucher.get_wanted_movements` for payment orders. As a side effect this also computes the :attr:`total` field and saves the voucher. """ # dd.logger.info("20151211 cosi.PaymentOrder.get_wanted_movements()") acc = self.journal.account if not acc: warn_jnl_account(self.journal) # TODO: what if the needs_partner of the journal's account # is not checked? Shouldn't we raise an error here? amount, movements_and_items = self.get_finan_movements() if abs(amount) > MAX_AMOUNT: # dd.logger.warning("Oops, {} is too big ({})".format(amount, self)) raise Exception("Oops, {} is too big ({})".format(amount, self)) return self.total = self.journal.dc.normalized_amount( -amount) # PaymentOrder.get_wanted_movements() # if self.journal.dc == DC.debit: # PaymentOrder.get_wanted_movements() # self.total = -amount # else: # self.total = amount item_partner = self.journal.partner is None for m, i in movements_and_items: yield m if item_partner: yield self.create_movement( i, (acc, None), m.project, -m.amount, # 20201219 PaymentOrder.get_wanted_movements partner=m.partner, match=i.get_match()) if not item_partner: yield self.create_movement( None, (acc, None), None, -amount, # 20201219 PaymentOrder.get_wanted_movements partner=self.journal.partner, match=self) # 20191226 partner=self.journal.partner, match=self.get_default_match()) # no need to save() because this is called during set_workflow_state() # self.full_clean() # self.save() def add_item_from_due(self, obj, **kwargs): # if obj.bank_account is None: # return i = super(PaymentOrder, self).add_item_from_due(obj, **kwargs) i.bank_account = obj.bank_account return i
class FinancialVoucherItem(VoucherItem, SequencedVoucherItem, ProjectRelated, Matching): class Meta: abstract = True verbose_name = _("Item") verbose_name_plural = _("Items") app_label = "finan" amount = dd.PriceField(_("Amount"), default=ZERO, null=False) # dc = DebitOrCreditField() remark = models.CharField(_("Your reference"), max_length=200, blank=True) account = dd.ForeignKey('ledger.Account', blank=True, null=True) partner = dd.ForeignKey('contacts.Partner', blank=True, null=True) quick_search_fields = 'remark account__ref account__name partner__name' @dd.chooser(simple_values=True) def match_choices(cls, voucher, partner): fkw = {} if not dd.plugins.finan.suggest_future_vouchers: fkw.update(value_date__lte=voucher.entry_date) return cls.get_match_choices(voucher.journal, partner, **fkw) def add_item_from_due(self, obj, **kwargs): return self.voucher.add_item_from_due(obj, **kwargs) # def get_default_match(self): # 20191226 def __str__(self): """Used as `match` when no explicit match is specified for this movement. """ if self.voucher_id and self.voucher.journal_id: return "%s %s:%s" % (self.voucher.journal.ref, self.voucher.number, self.seqno) # return str(self.date) return models.Model.__str__(self) # return super(FinancialVoucherItem, self).__str__() def get_siblings(self): return self.voucher.items.all() def collect_suggestions(self, ar, flt): suggestions = list( ledger.get_due_movements(self.voucher.journal.dc, flt)) # dd.logger.info("20210106 %s", suggestions) if len(suggestions) == 0: self.match = "" elif len(suggestions) == 1: self.fill_suggestion(suggestions[0]) elif ar: self.match = _("{} suggestions").format(len(suggestions)) # def ok(ar2): # # self.fill_suggestion(suggestions[0]) # # self.set_grouper(suggestions) # ar2.error(_("Oops, not implemented.")) # return # elems = ["Cool! ", E.b(str(self.partner)), # " has ", E.b(str(len(suggestions))), # " suggestions! Click "] # ba = ar.actor.get_action_by_name('suggest') # elems.append(ar.action_button(ba, self)) # elems.append(".") # html = E.p(*elems) # # dd.logger.info("20160526 %s", E.tostring(html)) # ar.success(E.tostring(html), alert=True) # # ar.confirm(ok, E.tostring(html)) def match_changed(self, ar): if not self.match or not self.voucher.journal.auto_fill_suggestions: return flt = dict(match=self.match, cleared=False) if not dd.plugins.finan.suggest_future_vouchers: flt.update(value_date__lte=self.voucher.entry_date) self.collect_suggestions(ar, models.Q(**flt)) self.guess_amount() def partner_changed(self, ar): # dd.logger.info("20210106 FinancialMixin.partner_changed %s", self.amount) if self.amount: return if not self.partner or not self.voucher.journal.auto_fill_suggestions: return flt = dict(partner=self.partner, cleared=False) if not dd.plugins.finan.suggest_future_vouchers: flt.update(value_date__lte=self.voucher.entry_date) if self.match: flt.update(match=self.match) self.collect_suggestions(ar, models.Q(**flt)) self.guess_amount() def debit_changed(self, ar): self.guess_amount() def credit_changed(self, ar): self.guess_amount() def amount_changed(self, ar): self.guess_amount() def account_changed(self, ar): if self.amount: return if not self.account: return if self.account.default_amount: self.amount = self.voucher.journal.dc.normalized_amount( self.account.default_amount) return if self.account.clearable and self.voucher.journal.auto_fill_suggestions: flt = dict(account=self.account, partner=self.partner, cleared=False) if not dd.plugins.finan.suggest_future_vouchers: flt.update(value_date__lte=self.voucher.entry_date) if self.match: flt.update(match=self.match) self.collect_suggestions(ar, models.Q(**flt)) self.guess_amount() def get_partner(self): return self.partner or self.project def fill_suggestion(self, due): """Fill the fields of this item from the given suggestion (a `DueMovement` instance). """ for k, v in self.voucher.due2itemdict(due).items(): setattr(self, k, v) def guess_amount(self): if self.amount: return self.amount = ZERO if self.voucher.auto_compute_amount: q = self.voucher.items.exclude(id=self.id).annotate( models.Sum('amount')) # print("20210108", q[0].amount__sum) self.amount = -myround(q[0].amount__sum) def full_clean(self, *args, **kwargs): if self.amount is None: self.amount = ZERO # just in case... if True: # temporarily deactivated for data migration problems = list(FinancialVoucherItemChecker.check_instance(self)) if len(problems): raise ValidationError("20181120 {}".format('\n'.join( [str(p[1]) for p in problems]))) # dd.logger.info("20151117 FinancialVoucherItem.full_clean a %s", self.amount) super(FinancialVoucherItem, self).full_clean(*args, **kwargs)
def get_model_field(self): return dd.PriceField( self.text, default=Decimal, editable=self.editable, help_text=self.help_text)
class Movement(ProjectRelated): """Represents an accounting movement in the ledger. .. attribute:: value_date The date at which this movement is to be entered into the ledger. This is usually the voucher's :attr:`entry_date <lino_cosi.lib.ledger.models.Voucher.entry_date>`, except e.g. for bank statements where each item can have its own value date. .. attribute:: voucher Pointer to the :class:`Voucher` who caused this movement. .. attribute:: partner Pointer to the partner involved in this movement. This may be blank. .. attribute:: seqno Sequential number within a voucher. .. attribute:: account Pointer to the :class:`Account` that is being moved by this movement. .. attribute:: amount .. attribute:: dc .. attribute:: match Pointer to the :class:`Movement` that is being cleared by this movement. .. attribute:: cleared Whether .. attribute:: voucher_partner A virtual field which returns the *partner of the voucher*. For incoming invoices this is the supplier, for outgoing invoices this is the customer, for financial vouchers this is empty. .. attribute:: voucher_link A virtual field which shows a link to the voucher. .. attribute:: match_link A virtual field which shows a clickable variant of the match string. Clicking it will open a table with all movements having that match. """ allow_cascaded_delete = ['voucher'] class Meta: app_label = 'ledger' verbose_name = _("Movement") verbose_name_plural = _("Movements") voucher = models.ForeignKey(Voucher) partner = dd.ForeignKey( 'contacts.Partner', related_name="%(app_label)s_%(class)s_set_by_partner", blank=True, null=True) seqno = models.IntegerField(_("Seq.No.")) account = dd.ForeignKey('accounts.Account') amount = dd.PriceField(default=0) dc = DebitOrCreditField() match = models.CharField(_("Match"), blank=True, max_length=20) # match = MatchField(blank=True, null=True) cleared = models.BooleanField(_("Cleared"), default=False) # 20160327: rename "satisfied" to "cleared" value_date = models.DateField(_("Value date"), null=True, blank=True) @dd.chooser(simple_values=True) def match_choices(cls, partner, account): qs = cls.objects.filter(partner=partner, account=account, cleared=False) qs = qs.order_by('value_date') return qs.values_list('match', flat=True) def select_text(self): v = self.voucher.get_mti_leaf() return "%s (%s)" % (v, v.entry_date) @dd.virtualfield(dd.PriceField(_("Debit"))) def debit(self, ar): if self.dc: return None return self.amount @dd.virtualfield(dd.PriceField(_("Credit"))) def credit(self, ar): if self.dc: return self.amount return None @dd.displayfield(_("Voucher")) def voucher_link(self, ar): if ar is None: return '' return ar.obj2html(self.voucher.get_mti_leaf()) @dd.displayfield(_("Voucher partner")) def voucher_partner(self, ar): if ar is None: return '' voucher = self.voucher.get_mti_leaf() p = voucher.get_partner() if p is None: return '' return ar.obj2html(p) @dd.displayfield(_("Match")) def match_link(self, ar): if ar is None or not self.match: return '' sar = rt.modules.ledger.MovementsByMatch.request( master_instance=self.match, parent=ar) return sar.ar2button(label=self.match) #~ @dd.displayfield(_("Matched by")) #~ def matched_by(self,ar): #~ elems = [obj.voucher_link(ar) for obj in Movement.objects.filter(match=self)] #~ return E.div(*elems) def get_siblings(self): return self.voucher.movement_set.order_by('seqno') #~ return self.__class__.objects.filter().order_by('seqno') def __str__(self): return "%s.%d" % (unicode(self.voucher), self.seqno) # def get_match(self): # return self.match or str(self.voucher) @classmethod def get_balance(cls, dc, qs): bal = ZERO for mvt in qs: if mvt.dc == dc: bal += mvt.amount else: bal -= mvt.amount return bal @classmethod def balance_info(cls, dc, **kwargs): qs = cls.objects.filter(**kwargs) qs = qs.order_by('value_date') bal = ZERO s = '' for mvt in qs: amount = mvt.amount if mvt.dc == dc: bal -= amount s += ' -' + str(amount) else: bal += amount s += ' +' + str(amount) s += " ({0}) ".format(mvt.voucher) # s += " ({0} {1}) ".format( # mvt.voucher, # dd.fds(mvt.voucher.voucher_date)) if bal: return s + "= " + str(bal) return '' if False: mvts = [] for dm in get_due_movements(CREDIT, partner=self.pupil): s = dm.match s += " [{0}]".format(day_and_month(dm.due_date)) s += " (" s += ', '.join([str(i.voucher) for i in dm.debts]) if len(dm.payments): s += " - " s += ', '.join([str(i.voucher) for i in dm.payments]) s += "): {0}".format(dm.balance) mvts.append(s) return '\n'.join(mvts)
class Account(mixins.BabelNamed, mixins.Sequenced, mixins.Referrable): """An **account** is an item of an account chart used to collect ledger transactions or other accountable items. .. attribute:: name The multilingual designation of this account, as the users see it. .. attribute:: group The *account group* to which this account belongs. Points to an instance of :class:`Group`. If this field is empty, the account won't appear in certain reports. .. attribute:: seqno The sequence number of this account within its :attr:`group`. .. attribute:: ref An optional unique name which can be used to reference a given account. .. attribute:: type The *account type* of this account. This points to an item of :class:`AccountTypes <lino_cosi.lib.accounts.choicelists.AccountTypes>`. .. attribute:: needs_partner Whether bookings to this account need a partner specified. This causes the contra entry of financial documents to be detailed (i.e. one for every item) or not (i.e. a single contra entry per voucher, without project nor partner). .. attribute:: default_amount The default amount to book in bank statements or journal entries when this account has been selected manually. The default booking direction is that of the :attr:`type`. """ ref_max_length = settings.SITE.plugins.accounts.ref_length class Meta: verbose_name = _("Account") verbose_name_plural = _("Accounts") ordering = ['ref'] group = models.ForeignKey('accounts.Group', blank=True, null=True) type = AccountTypes.field() # blank=True) needs_partner = models.BooleanField(_("Needs partner"), default=False) clearable = models.BooleanField(_("Clearable"), default=False) # default_dc = DebitOrCreditField(_("Default booking direction")) default_amount = dd.PriceField(_("Default amount"), blank=True, null=True) def full_clean(self, *args, **kw): if self.group_id is not None: if not self.ref: qs = rt.modules.accounts.Account.objects.all() self.ref = str(qs.count() + 1) if not self.name: self.name = self.group.name self.type = self.group.account_type # if self.default_dc is None: # self.default_dc = self.type.dc super(Account, self).full_clean(*args, **kw) def __str__(self): return "(%(ref)s) %(title)s" % dict( ref=self.ref, title=settings.SITE.babelattr(self, 'name'))
class DebtorsCreditors(dd.VirtualTable): required_roles = dd.login_required(AccountingReader) auto_fit_column_widths = True column_names = "age due_date partner partner_id balance vouchers" display_mode = 'html' abstract = True parameters = mixins.Today() # params_layout = "today" d_or_c = NotImplementedError @classmethod def get_data_rows(self, ar): rows = [] mi = ar.master_instance if mi is None: # called directly from main menu if ar.param_values is None: return rows end_date = ar.param_values.today else: # called from Situation report end_date = mi.today get_due_movements = rt.models.ledger.get_due_movements qs = rt.models.contacts.Partner.objects.order_by('name') for row in qs: row._balance = ZERO row._due_date = None row._expected = tuple( get_due_movements( self.d_or_c, models.Q(partner=row, value_date__lte=end_date))) for dm in row._expected: row._balance += dm.balance if dm.due_date is not None: if row._due_date is None or row._due_date > dm.due_date: row._due_date = dm.due_date # logger.info("20140105 %s %s", row, dm) if row._balance > ZERO: rows.append(row) def k(a): return a._due_date rows.sort(key=k) return rows # @dd.displayfield(_("Partner")) # def partner(self, row, ar): # return ar.obj2html(row) @dd.virtualfield(dd.ForeignKey('contacts.Partner')) def partner(self, row, ar): return row @dd.virtualfield(models.IntegerField(_("ID"))) def partner_id(self, row, ar): return row.pk @dd.virtualfield(dd.PriceField(_("Balance"))) def balance(self, row, ar): return row._balance @dd.virtualfield(models.DateField(_("Due date"))) def due_date(self, row, ar): return row._due_date @dd.virtualfield(models.IntegerField(_("Age"))) def age(self, row, ar): dd = ar.param_values.today - row._due_date return dd.days @dd.displayfield(_("Vouchers")) def vouchers(self, row, ar): matches = [dm.match for dm in row._expected] return E.span(', '.join(matches))
class ItemEntry(ReportEntry): class Meta: app_label = 'sheets' abstract = dd.is_abstract_model(__name__, 'ItemEntry') verbose_name = _("Sheet item entry") verbose_name_plural = _("Sheet item entries") # allow_cascaded_delete = ['item'] item = dd.ForeignKey('sheets.Item') @dd.displayfield(_("Description")) def description(self, ar=None): # print(20180831, ar.renderer, ar.user) return self.item.description @classmethod def get_collectors(cls): yield Collector(cls, 'item', 'account__sheet_item') @classmethod def setup_parameters(cls, fields): fields.setdefault( 'sheet_type', SheetTypes.field()) super(ItemEntry, cls).setup_parameters(fields) @classmethod def get_request_queryset(self, ar, **filter): qs = super(ItemEntry, self).get_request_queryset(ar, **filter) pv = ar.param_values if pv.sheet_type: qs = qs.filter(item__sheet_type=pv.sheet_type) return qs @classmethod def get_title_tags(self, ar): for t in super(ItemEntry, self).get_title_tags(ar): yield t pv = ar.param_values if pv.sheet_type: yield str(pv.sheet_type) @dd.virtualfield(dd.PriceField(_("Activa"))) def activa(self, ar): if self.item.dc is CREDIT: return self.new_balance().c @dd.virtualfield(dd.PriceField(_("Passiva"))) def passiva(self, ar): if self.item.dc is DEBIT: return self.new_balance().d # @dd.displayfield(_("Activa"), max_length=12) # def activa(self, ar): # if self.item.dc: # return self.value2html(ar) # @dd.displayfield(_("Passiva"), max_length=12) # def passiva(self, ar): # if not self.item.dc: # return self.value2html(ar) @dd.virtualfield(dd.PriceField(_("Expenses"))) def expenses(self, ar): if self.item.dc is DEBIT: return self.new_balance().d @dd.virtualfield(dd.PriceField(_("Revenues"))) def revenues(self, ar): if self.item.dc is CREDIT: return self.new_balance().c
class AccountBalances(dd.Table): editable = False required_roles = dd.login_required(AccountingReader) auto_fit_column_widths = True column_names = "description old_d old_c empty_column:1 during_d during_c empty_column:1 new_d new_c" display_mode = 'html' abstract = True params_panel_hidden = False use_as_default_table = False parameters = AccountingPeriodRange() params_layout = "start_period end_period" @classmethod def rowmvtfilter(self): raise NotImplementedError() @classmethod def get_request_queryset(self, ar, **kwargs): # see https://docs.djangoproject.com/en/1.11/ref/models/expressions/#using-aggregates-within-a-subquery-expression AccountingPeriod = rt.models.ledger.AccountingPeriod pv = ar.param_values sp = pv.start_period or AccountingPeriod.get_default_for_date( dd.today()) ep = pv.end_period or sp qs = super(AccountBalances, self).get_request_queryset(ar) flt = self.rowmvtfilter(ar) oldflt = dict() oldflt.update(flt) duringflt = dict() duringflt.update(flt) during_periods = AccountingPeriod.objects.filter(ref__gte=sp.ref, ref__lte=ep.ref) before_periods = AccountingPeriod.objects.filter(ref__lt=sp.ref) oldflt.update(voucher__accounting_period__in=before_periods) duringflt.update(voucher__accounting_period__in=during_periods) outer_link = self.model._meta.model_name def addann(kw, name, dc, flt): mvts = rt.models.ledger.Movement.objects.filter(dc=dc, **flt) mvts = mvts.order_by() mvts = mvts.values(outer_link) # this was the important thing mvts = mvts.annotate( total=Sum('amount', output_field=dd.PriceField())) mvts = mvts.values('total') kw[name] = Subquery(mvts, output_field=dd.PriceField()) kw = dict() addann(kw, 'old_d', DEBIT, oldflt) addann(kw, 'old_c', CREDIT, oldflt) addann(kw, 'during_d', DEBIT, duringflt) addann(kw, 'during_c', CREDIT, duringflt) qs = qs.annotate(**kw) qs = qs.exclude(old_d=ZERO, old_c=ZERO, during_d=ZERO, during_c=ZERO) # print("20170930 {}".format(qs.query)) return qs @classmethod def new_balance(cls, row): return Balance(row.old_d, row.old_c) + Balance(row.during_d, row.during_c) @classmethod def normal_dc(cls, row, ar): # raise NotImplementedError() return DEBIT # row.normal_dc @dd.displayfield(_("Reference")) def ref(self, row, ar): return row.ref @dd.displayfield(_("Description")) def description(self, row, ar): # print(20180831, ar.renderer, ar.user) return row.obj2href(ar) @dd.virtualfield(dd.PriceField(_("Old balance"))) def old_dc(self, row, ar): return Balance(row.old_d, row.old_c).value(self.normal_dc(row, ar)) @dd.virtualfield(dd.PriceField(_("Movements"))) def during_dc(self, row, ar): return Balance(row.during_d, row.during_c).value(self.normal_dc(row, ar)) @dd.virtualfield(dd.PriceField(_("New balance"))) def new_dc(self, row, ar): return self.new_balance(row).value(self.normal_dc(row, ar)) @dd.virtualfield(dd.PriceField(_("Debit before"))) def old_d(self, row, ar): return Balance(row.old_d, row.old_c).d @dd.virtualfield(dd.PriceField(_("Credit before"))) def old_c(self, row, ar): return Balance(row.old_d, row.old_c).c @dd.virtualfield(dd.PriceField(_("Debit"))) def during_d(self, row, ar): return row.during_d @dd.virtualfield(dd.PriceField(_("Credit"))) def during_c(self, row, ar): return row.during_c @dd.virtualfield(dd.PriceField(_("Debit after"))) def new_d(self, row, ar): return self.new_balance(row).d @dd.virtualfield(dd.PriceField(_("Credit after"))) def new_c(self, row, ar): return self.new_balance(row).c @dd.displayfield("", max_length=0) def empty_column(self, row, ar): return ''
class VatTotal(dd.Model): """Model mixin which defines the fields `total_incl`, `total_base` and `total_vat`. Used for both the document header (:class:`VatDocument`) and for each item (:class:`VatItemBase`). .. attribute:: total_incl A :class:`lino.core.fields.PriceField` which stores the total amount VAT *included*. .. attribute:: total_base A :class:`lino.core.fields.PriceField` which stores the total amount VAT *excluded*. .. attribute:: total_vat A :class:`lino.core.fields.PriceField` which stores the amount of VAT. """ class Meta: abstract = True # price = dd.PriceField(_("Total"),blank=True,null=True) total_incl = dd.PriceField(_("Total incl. VAT"), blank=True, null=True) total_base = dd.PriceField(_("Total excl. VAT"), blank=True, null=True) total_vat = dd.PriceField(_("VAT"), blank=True, null=True) _total_fields = set('total_vat total_base total_incl'.split()) # For internal use. This is the list of field names to disable # when `auto_compute_totals` is True. auto_compute_totals = False """Set this to `True` on subclasses who compute their totals automatically, i.e. the fields :attr:`total_base`, :attr:`total_vat` and :attr:`total_incl` are disabled. This is set to `True` for :class:`lino_cosi.lib.sales.models.SalesDocument`. """ def disabled_fields(self, ar): """Disable all three total fields if `auto_compute_totals` is set, otherwise disable :attr:`total_vat` if :attr:`VatRule.can_edit` is False. """ fields = super(VatTotal, self).disabled_fields(ar) if self.auto_compute_totals: fields |= self._total_fields else: rule = self.get_vat_rule() if rule is not None and not rule.can_edit: fields.add('total_vat') return fields def reset_totals(self, ar): pass def get_vat_rule(self): """Return the VAT rule for this voucher or voucher item. Called when user edits a total field in the document header when `auto_compute_totals` is False. """ return None def total_base_changed(self, ar): """Called when user has edited the `total_base` field. If total_base has been set to blank, then Lino fills it using :meth:`reset_totals`. If user has entered a value, compute :attr:`total_vat` and :attr:`total_incl` from this value using the vat rate. If there is no VatRule, `total_incl` and `total_vat` are set to None. If there are rounding differences, `total_vat` will get them. """ # logger.info("20150128 total_base_changed %r", self.total_base) if self.total_base is None: self.reset_totals(ar) if self.total_base is None: return rule = self.get_vat_rule() # logger.info("20150128 %r", rule) if rule is None: self.total_incl = None self.total_vat = None else: self.total_incl = myround(self.total_base * (ONE + rule.rate)) self.total_vat = self.total_incl - self.total_base def total_vat_changed(self, ar): """Called when user has edited the `total_vat` field. If it has been set to blank, then Lino fills it using :meth:`reset_totals`. If user has entered a value, compute :attr:`total_incl`. If there is no VatRule, `total_incl` is set to None. """ if self.total_vat is None: self.reset_totals(ar) if self.total_vat is None: return if self.total_base is None: self.total_base = ZERO self.total_incl = self.total_vat + self.total_base def total_incl_changed(self, ar): """Called when user has edited the `total_incl` field. If total_incl has been set to blank, then Lino fills it using :meth:`reset_totals`. If user enters a value, compute :attr:`total_base` and :attr:`total_vat` from this value using the vat rate. If there is no VatRule, `total_incl` should be disabled, so this method will never be called. If there are rounding differences, `total_vat` will get them. """ if self.total_incl is None: self.reset_totals(ar) if self.total_incl is None: return # assert not isinstance(self.total_incl,basestring) rule = self.get_vat_rule() if rule is None: self.total_base = None self.total_vat = None else: self.total_base = myround(self.total_incl / (ONE + rule.rate)) self.total_vat = myround(self.total_incl - self.total_base)
class AccountInvoice(BankAccount, Payable, RegistrableVoucher, Matching, ProjectRelated): class Meta: app_label = 'vatless' verbose_name = _("Invoice") verbose_name_plural = _("Invoices") state = VoucherStates.field(default='draft') amount = dd.PriceField(_("Amount"), blank=True, null=True) # _total_fields = set(['amount']) # """The list of field names to disable when `edit_totals` is # False. # # """ # # edit_totals = False # # def disabled_fields(self, ar): # """Disable all three total fields if `edit_totals` is False, # otherwise disable :attr:`total_vat` if # :attr:`VatRule.can_edit` is False. # # """ # fields = super(AccountInvoice, self).disabled_fields(ar) # if not self.edit_totals: # fields |= self._total_fields # return fields def get_partner(self): return self.partner or self.project def compute_totals(self): if self.pk is None: return base = Decimal() for i in self.items.all(): if i.amount is not None: base += i.amount self.amount = base def get_payable_sums_dict(self): tt = self.get_trade_type() sums = SumCollector() for i in self.items.order_by('seqno'): if i.amount: b = i.get_base_account(tt) if b is None: raise Exception("No base account for %s (amount is %r)" % (i, i.amount)) sums.collect(((b, i.get_ana_account()), i.project or self.project, None, None), i.amount) return sums def full_clean(self, *args, **kw): self.compute_totals() super(AccountInvoice, self).full_clean(*args, **kw) def before_state_change(self, ar, old, new): if new.name == 'registered': self.compute_totals() elif new.name == 'draft': pass super(AccountInvoice, self).before_state_change(ar, old, new)
class ExpectedMovements(dd.VirtualTable): row_height = 4 required_roles = dd.login_required(AccountingReader) label = _("Debts") icon_name = 'book_link' #~ column_names = 'match due_date debts payments balance' column_names = 'due_date:15 balance debts payments' auto_fit_column_widths = True # variable_row_height = True parameters = dd.ParameterPanel( date_until=models.DateField(_("Date until"), blank=True, null=True), trade_type=TradeTypes.field(blank=True), from_journal=dd.ForeignKey('ledger.Journal', blank=True), for_journal=dd.ForeignKey('ledger.Journal', blank=True, verbose_name=_("Clearable by")), account=dd.ForeignKey('ledger.Account', blank=True), partner=dd.ForeignKey('contacts.Partner', blank=True), project=dd.ForeignKey(dd.plugins.ledger.project_model, blank=True), show_sepa=dd.YesNo.field(_("With SEPA"), blank=True), same_dc=dd.YesNo.field(_("Same D/C"), blank=True), ) params_layout = """ trade_type date_until from_journal for_journal project partner account show_sepa same_dc""" @classmethod def get_dc(cls, ar=None): return CREDIT @classmethod def get_data_rows(cls, ar, **flt): #~ if ar.param_values.journal: #~ pass pv = ar.param_values # if pv is None: # raise Exception("No pv in %s" % ar) if pv.trade_type: flt.update(account=pv.trade_type.get_main_account()) if pv.partner: flt.update(partner=pv.partner) if pv.account: flt.update(account=pv.account) if pv.project: flt.update(project=pv.project) if pv.show_sepa == dd.YesNo.yes: flt.update(partner__sepa_accounts__primary=True) elif pv.show_sepa == dd.YesNo.no: flt.update(partner__sepa_accounts__primary__isnull=True) if pv.same_dc == dd.YesNo.yes: flt.update(dc=cls.get_dc(ar)) elif pv.same_dc == dd.YesNo.no: flt.update(dc=not cls.get_dc(ar)) if pv.date_until is not None: flt.update(value_date__lte=pv.date_until) if pv.for_journal is not None: accounts = rt.models.ledger.Account.objects.filter( matchrule__journal=pv.for_journal).distinct() flt.update(account__in=accounts) if pv.from_journal is not None: flt.update(voucher__journal=pv.from_journal) flt = models.Q(**flt) if ar.quick_search: flt &= rt.models.contacts.Partner.quick_search_filter( ar.quick_search, prefix='partner__') return rt.models.ledger.get_due_movements(cls.get_dc(ar), flt) @classmethod def get_pk_field(self): return rt.models.ledger.Movement._meta.pk @classmethod def get_row_by_pk(cls, ar, pk): # this is tricky. # for i in ar.data_iterator: # if i.id == pk: # return i # raise Exception("Not found: %s in %s" % (pk, ar)) mvt = rt.models.ledger.Movement.objects.get(pk=pk) dm = rt.models.ledger.DueMovement(cls.get_dc(ar), mvt) dm.collect_all() return dm @dd.displayfield(_("Info")) def info(self, row, ar): elems = [] if row.project: elems.append(ar.obj2html(row.project)) if row.partner: elems.append(ar.obj2html(row.partner)) # elems.append(row.partner.address) if row.bank_account: elems.append(ar.obj2html(row.bank_account)) if row.account: elems.append(ar.obj2html(row.account)) # return E.span(*join_elems(elems, ' / ')) return E.span(*join_elems(elems, E.br)) # return E.span(*elems) @dd.displayfield(_("Match")) def match(self, row, ar): return row.match @dd.virtualfield( models.DateField( _("Due date"), help_text=_("Due date of the eldest debt in this match group"))) def due_date(self, row, ar): return row.due_date @dd.displayfield(_("Debts"), help_text=_("List of invoices in this match group")) def debts(self, row, ar): return E.span(*join_elems([ # E.p(...) until 20150128 ar.obj2html(i.voucher.get_mti_leaf()) for i in row.debts ])) @dd.displayfield(_("Payments"), help_text=_("List of payments in this match group")) def payments(self, row, ar): return E.span(*join_elems([ # E.p(...) until 20150128 ar.obj2html(i.voucher.get_mti_leaf()) for i in row.payments ])) @dd.virtualfield(dd.PriceField(_("Balance"))) def balance(self, row, ar): return row.balance @dd.virtualfield(dd.ForeignKey('contacts.Partner')) def partner(self, row, ar): return row.partner @dd.virtualfield(dd.ForeignKey(dd.plugins.ledger.project_model)) def project(self, row, ar): return row.project @dd.virtualfield(dd.ForeignKey('ledger.Account')) def account(self, row, ar): return row.account @dd.virtualfield( dd.ForeignKey('sepa.Account', verbose_name=_("Bank account"))) def bank_account(self, row, ar): return row.bank_account