class Product(mixins.BabelNamed): """A product is something you can sell or buy. .. attribute:: description .. attribute:: cat .. attribute:: delivery_unit """ class Meta: app_label = 'products' verbose_name = _("Product") verbose_name_plural = _("Products") abstract = dd.is_abstract_model(__name__, 'Product') description = dd.BabelTextField(verbose_name=_("Long description"), blank=True, null=True) cat = models.ForeignKey(ProductCat, verbose_name=_("Category"), blank=True, null=True) delivery_unit = DeliveryUnit.field( default=DeliveryUnit.piece.as_callable()) if vat: vat_class = vat.VatClasses.field(blank=True) else: vat_class = dd.DummyField()
class Concept(mixins.BabelNamed): """A word and its translation in different languages. """ class Meta: verbose_name = _("Concept") verbose_name_plural = _("Concepts") abbr = dd.BabelCharField(_("Abbreviation"), max_length=30, blank=True) wikipedia = dd.BabelCharField(_("Wikipedia"), max_length=200, blank=True) definition = dd.BabelTextField(_("Definition"), blank=True) is_jargon_domain = models.BooleanField( _("Jargon domain"), help_text= _("Whether this concept designates a domain of specialized vocabulary." ), default=False) def summary_row(self, ar=None): if self.abbr: return [ "%s (%s)" % (dd.babelattr(self, 'name'), dd.babelattr(self, 'abbr')) ] return [dd.babelattr(self, 'name')]
class EventType(mixins.BabelNamed): class Meta: app_label = 'notes' verbose_name = pgettext_lazy(u"notes", u"Event Type") verbose_name_plural = _("Event Types") remark = models.TextField(verbose_name=_("Remark"), blank=True) body = dd.BabelTextField(_("Body"), blank=True, format='html')
class EventType(mixins.BabelNamed): """ A possible choice for :attr:`Note.event_type`. """ class Meta: app_label = 'notes' verbose_name = pgettext_lazy(u"notes", u"Event Type") verbose_name_plural = _("Event Types") remark = models.TextField(verbose_name=_("Remark"), blank=True) body = dd.BabelTextField(_("Body"), blank=True, format='html')
class Topic(StructuredReferrable, BabelNamed): ref_max_length = 5 class Meta: app_label = 'topics' verbose_name = _("Topic") verbose_name_plural = _("Topics") abstract = dd.is_abstract_model(__name__, 'Topic') description_text = dd.BabelTextField(verbose_name=_("Long description"), blank=True, null=True)
class Product(mixins.BabelNamed, mixins.Referrable): class Meta: verbose_name = _("Product") verbose_name_plural = _("Products") abstract = dd.is_abstract_model(__name__, 'Product') description = dd.BabelTextField(verbose_name=_("Long description"), blank=True, null=True) cat = models.ForeignKey(ProductCat, verbose_name=_("Category"), blank=True, null=True) if vat: vat_class = vat.VatClasses.field(blank=True) else: vat_class = dd.DummyField()
class Topic(BabelNamed, Referrable): """A topic is something somebody can be interested in. .. attribute:: ref .. attribute:: description """ class Meta: app_label = 'topics' verbose_name = _("Topic") verbose_name_plural = _("Topics") abstract = dd.is_abstract_model(__name__, 'Topic') description = dd.BabelTextField(verbose_name=_("Long description"), blank=True, null=True) topic_group = dd.ForeignKey('topics.TopicGroup', blank=True, null=True)
class Page(Referrable, Hierarchical, Sequenced, Publishable): class Meta: verbose_name = _("Node") verbose_name_plural = _("Nodes") title = dd.BabelCharField(_("Title"), max_length=200, blank=True) body = dd.BabelTextField(_("Body"), blank=True, format='plain') raw_html = models.BooleanField(_("raw html"), default=False) @classmethod def get_dashboard_items(cls, user): obj = cls.get_by_ref('index', None) if obj is not None: yield obj def get_absolute_url(self, **kwargs): if self.ref: if self.ref != 'index': return dd.plugins.pages.build_plain_url(self.ref, **kwargs) return dd.plugins.pages.build_plain_url(**kwargs) def get_sidebar_caption(self): if self.title: return dd.babelattr(self, 'title') if self.ref == 'index': return str(_('Home')) if self.ref: return self.ref return str(self.id) #~ if self.ref or self.parent: #~ return self.ref #~ return unicode(_('Home')) def get_sidebar_item(self, request, other): kw = dict() add_user_language(kw, request) url = self.get_absolute_url(**kw) a = E.a(self.get_sidebar_caption(), href=url) if self == other: return E.li(a, **{'class': 'active'}) return E.li(a) def get_sidebar_html(self, request): items = [] #~ loop over top-level nodes for n in Page.objects.filter(parent__isnull=True).order_by('seqno'): #~ items += [li for li in n.get_sidebar_items(request,self)] items.append(n.get_sidebar_item(request, self)) if self.is_parented(n): children = [] for ch in n.children.order_by('seqno'): children.append(ch.get_sidebar_item(request, self)) if len(children): items.append(E.ul(*children, **{'class': 'nav nav-list'})) e = E.ul(*items, **{'class': 'nav nav-list'}) return tostring_pretty(e) def get_sidebar_menu(self, request): qs = Page.objects.filter(parent__isnull=True) #~ qs = self.children.all() yield ('/', 'index', str(_('Home'))) #~ yield ('/downloads/', 'downloads', 'Downloads') #~ yield ('/about', 'about', 'About') #~ if qs is not None: for obj in qs: if obj.ref and obj.title: yield ('/' + obj.ref, obj.ref, dd.babelattr(obj, 'title'))
class Course(Reservation, Duplicable): """A Course is a group of pupils that regularily meet with a given teacher in a given room to speak about a given subject. The subject of a course is expressed by the :class:`Line`. Notes about automatic event generation: - When an automatically generated event is to be moved to another date, e.g. because it falls into a vacation period, then you simply change it's date. Lino will automatically adapt all subsequent events. - Marking an automatically generated event as "Cancelled" will not create a replacement event. .. attribute:: enrolments_until .. attribute:: max_places Available places. The maximum number of participants to allow in this course. .. attribute:: free_places Number of free places. .. attribute:: requested Number of requested places. .. attribute:: confirmed Number of confirmed places. """ class Meta: app_label = 'courses' abstract = dd.is_abstract_model(__name__, 'Course') verbose_name = _("Activity") verbose_name_plural = _('Activities') line = models.ForeignKey('courses.Line') teacher = models.ForeignKey(teacher_model, verbose_name=_("Instructor"), blank=True, null=True) #~ room = models.ForeignKey(Room,blank=True,null=True) slot = models.ForeignKey(Slot, blank=True, null=True) description = dd.BabelTextField(_("Description"), blank=True) remark = models.TextField(_("Remark"), blank=True) quick_search_fields = 'name line__name line__topic__name' state = CourseStates.field(default=CourseStates.draft.as_callable) max_places = models.PositiveIntegerField( pgettext("in a course", "Available places"), help_text=("Maximum number of participants"), blank=True, null=True) name = models.CharField(_("Designation"), max_length=100, blank=True) enrolments_until = models.DateField(_("Enrolments until"), blank=True, null=True) def on_duplicate(self, ar, master): self.state = CourseStates.draft super(Course, self).on_duplicate(ar, master) @classmethod def get_registrable_fields(cls, site): for f in super(Course, cls).get_registrable_fields(site): yield f yield 'line' yield 'teacher' yield 'name' yield 'enrolments_until' def __str__(self): if self.name: return self.name if self.room is None: return "%s (%s)" % (self.line, dd.fds(self.start_date)) # Note that we cannot use super() with # python_2_unicode_compatible return "%s (%s %s)" % (self.line, dd.fds(self.start_date), self.room) def get_detail_action(self, ar): """Custom :meth:`get_detail_action <lino.core.model.Model.get_detail_action>` because the detail_layout to use depends on the course's line's `course_area`. """ if self.line_id: area = self.line.course_area if area: table = rt.actors.resolve(area.courses_table) a = table.detail_action if ar is None or a.get_view_permission(ar.get_user().profile): return a return None return super(Course, self).get_detail_action(ar) def update_cal_from(self, ar): """Note: if recurrency is weekly or per_weekday, actual start may be later than self.start_date """ # if self.state in (CourseStates.draft, CourseStates.cancelled): # if self.state == CourseStates.cancelled: # ar.info("No start date because state is %s", self.state) # return None return self.start_date def update_cal_event_type(self): return self.line.event_type def update_cal_summary(self, i): label = dd.babelattr(self.line.event_type, 'event_label') return "%s %d" % (label, i) def suggest_cal_guests(self, event): """Look up enrolments of this course and suggest them as guests.""" # logger.info("20140314 suggest_guests") Guest = rt.modules.cal.Guest Enrolment = rt.models.courses.Enrolment if self.line is None: return gr = self.line.guest_role if gr is None: return # fkw = dict(course=self) # states = (EnrolmentStates.requested, EnrolmentStates.confirmed) # fkw.update(state__in=states) qs = Enrolment.objects.filter(course=self).order_by( *dd.plugins.courses.pupil_name_fields) for obj in qs: if obj.is_guest_for(event): yield Guest(event=event, partner=obj.pupil, role=gr) def full_clean(self, *args, **kw): if self.line_id is not None: if self.id is None: descs = dd.field2kw(self.line, 'description') descs = dd.babelkw('description', **descs) for k, v in descs.items(): setattr(self, k, v) if self.every_unit is None: self.every_unit = self.line.every_unit if self.every is None: self.every = self.line.every # if self.enrolments_until is None: # self.enrolments_until = self.start_date # if self.id is not None: # if self.enrolments_until is None: # qs = self.get_existing_auto_events() # if qs.count(): # self.enrolments_until = qs[0].start_date super(Course, self).full_clean(*args, **kw) def before_auto_event_save(self, event): """ Sets room and start_time for automatic events. This is a usage example for :meth:`EventGenerator.before_auto_event_save <lino_xl.lib.cal.models.EventGenerator.before_auto_event_save>`. """ #~ logger.info("20131008 before_auto_event_save") assert not settings.SITE.loading_from_dump assert event.owner == self #~ event = instance if event.is_user_modified(): return #~ if event.is_fixed_state(): return #~ course = event.owner #~ event.project = self event.course = self event.room = self.room if self.slot: event.start_time = self.slot.start_time event.end_time = self.slot.end_time else: event.start_time = self.start_time event.end_time = self.end_time # @dd.displayfield(_("Info")) # def info(self, ar): # if ar is None: # return '' # return ar.obj2html(self) def get_overview_elems(self, ar): elems = [] elems.append(ar.obj2html(self)) if self.teacher_id: elems.append(" / ") elems.append(ar.obj2html(self.teacher)) return elems #~ @dd.displayfield(_("Where")) #~ def where_text(self,ar): # ~ return unicode(self.room) # .company.city or self.company) @dd.displayfield(_("Events")) def events_text(self, ar=None): return ', '.join([ dd.plugins.courses.day_and_month(e.start_date) for e in self.events_by_course.order_by('start_date') ]) @property def events_by_course(self): ct = rt.modules.contenttypes.ContentType.objects.get_for_model( self.__class__) return rt.modules.cal.Event.objects.filter(owner_type=ct, owner_id=self.id) def get_places_sum(self, today=None, **flt): Enrolment = rt.models.courses.Enrolment PeriodEvents = rt.modules.system.PeriodEvents qs = Enrolment.objects.filter(course=self, **flt) rng = DatePeriodValue(today or dd.today(), None) qs = PeriodEvents.active.add_filter(qs, rng) # logger.info("20160502 %s", qs.query) res = qs.aggregate(models.Sum('places')) # logger.info("20140819 %s", res) return res['places__sum'] or 0 def get_free_places(self, today=None): return self.max_places - self.get_used_places(today) def get_used_places(self, today=None): states = EnrolmentStates.filter(uses_a_place=True) return self.get_places_sum(today, state__in=states) # @dd.displayfield(_("Free places"), max_length=5) @dd.virtualfield(models.IntegerField(_("Free places"))) def free_places(self, ar=None): if not self.max_places: return None # _("Unlimited") return self.get_free_places() @dd.virtualfield(models.IntegerField(_("Requested"))) def requested(self, ar): return self.get_places_sum(state=EnrolmentStates.requested) # pv = dict(start_date=dd.today()) # pv.update(state=EnrolmentStates.requested) # return rt.actors.courses.EnrolmentsByCourse.request( # self, param_values=pv) @dd.virtualfield(models.IntegerField(_("Confirmed"))) def confirmed(self, ar): return self.get_places_sum(state=EnrolmentStates.confirmed) # pv = dict(start_date=dd.today()) # pv.update(state=EnrolmentStates.confirmed) # return rt.actors.courses.EnrolmentsByCourse.request( # self, param_values=pv) @dd.requestfield(_("Enrolments")) def enrolments(self, ar): return self.get_enrolments(start_date=dd.today()) def get_enrolments(self, **pv): # pv = dict(start_date=sd, end_date=dd.today()) return rt.actors.courses.EnrolmentsByCourse.request(self, param_values=pv) @dd.virtualfield(dd.HtmlBox(_("Presences"))) def presences_box(self, ar): # not finished if ar is None: return '' pv = ar.param_values # if not pv.start_date or not pv.end_date: # return '' events = self.events_by_course.order_by('start_date') events = rt.modules.system.PeriodEvents.started.add_filter(events, pv) return "TODO: copy logic from presence_sheet.wk.html"
class Line(Referrable, Duplicable, ExcerptTitle): """An **activity line** (or **series**) groups courses into a configurable list of categories. We chose the word "line" instead of "series" because it has a plural form (not sure whether this idea was so cool). .. attribute:: name The designation of this activity line as seen by the user e.g. when selecting the line. One field for every :attr:`language <lino.core.site.Site.language>`. .. attribute:: excerpt_title The text to print as title in enrolments. See also :attr:`lino_xl.lib.excerpts.mixins.ExcerptTitle.excerpt_title`. .. attribute:: body_template The body template to use when printing an activity of this line. Leave empty to use the site's default (defined by `body_template` on the :class:`lino_xl.lib.excerpts.models.ExcerptType` for :class:`Course`) .. attribute:: course_area Pointer to :class:`CourseAreas`. This is used only when an application defines several variants of :class:`EnrolmentsByPupil`. """ class Meta: app_label = 'courses' abstract = dd.is_abstract_model(__name__, 'Line') verbose_name = pgettext("singular form", "Activity line") verbose_name_plural = pgettext("plural form", 'Activity lines') # ref = dd.NullCharField(_("Reference"), max_length=30, unique=True) course_area = CourseAreas.field(default=CourseAreas.default.as_callable) # default=CourseAreas.get_lazy('default') topic = models.ForeignKey(Topic, blank=True, null=True) description = dd.BabelTextField(_("Description"), blank=True) every_unit = Recurrencies.field(_("Recurrency"), default=Recurrencies.weekly.as_callable, blank=True) # iCal:DURATION every = models.IntegerField(_("Repeat every"), default=1) event_type = dd.ForeignKey( 'cal.EventType', null=True, blank=True, help_text=_("The type of calendar events to be generated. " "If this is empty, no calendar events will be generated.")) fee = dd.ForeignKey('products.Product', blank=True, null=True, verbose_name=_("Participation fee"), related_name='lines_by_fee') guest_role = dd.ForeignKey( "cal.GuestRole", blank=True, null=True, verbose_name=_("Manage presences as"), help_text=_("The default guest role for particpants of events for " "courses in this series. " "Leave empty if you don't want any presence management.")) options_cat = dd.ForeignKey('products.ProductCat', verbose_name=_("Options category"), related_name="courses_lines_by_options_cat", blank=True, null=True) fees_cat = dd.ForeignKey('products.ProductCat', verbose_name=_("Fees category"), related_name="courses_lines_by_fees_cat", blank=True, null=True) body_template = models.CharField(max_length=200, verbose_name=_("Body template"), blank=True, help_text="The body template to use when " "printing a course of this series. " "Leave empty to use the site's default.") def __str__(self): name = dd.babelattr(self, 'name') if self.ref: return "{0} ({1})".format(self.ref, name) return name # return "{0} #{1}".format(self._meta.verbose_name, self.pk) @dd.chooser() def fee_choices(cls, fees_cat): Product = rt.modules.products.Product if not fees_cat: return Product.objects.none() return Product.objects.filter(cat=fees_cat) @dd.chooser(simple_values=True) def body_template_choices(cls): return dd.plugins.jinja.list_templates( '.body.html', rt.models.courses.Enrolment.get_template_group(), 'excerpts')
class Page(mixins.Referrable, mixins.Hierarchical, mixins.Sequenced): """ Deserves more documentation. """ class Meta: verbose_name = _("Node") verbose_name_plural = _("Nodes") title = dd.BabelCharField(_("Title"), max_length=200, blank=True) body = dd.BabelTextField(_("Body"), blank=True, format='plain') raw_html = models.BooleanField(_("raw html"), default=False) def get_absolute_url(self): if self.ref: if self.ref != 'index': return PAGES.build_plain_url(self.ref) return PAGES.build_plain_url() def get_sidebar_caption(self): if self.title: return dd.babelattr(self, 'title') if self.ref == 'index': return unicode(_('Home')) if self.ref: return self.ref return str(self.id) #~ if self.ref or self.parent: #~ return self.ref #~ return unicode(_('Home')) def get_sidebar_item(self, request, other): a = E.a(self.get_sidebar_caption(), href=self.get_absolute_url()) if self == other: return E.li(a, class_='active') return E.li(a) def get_sidebar_html(self, request): items = [] #~ loop over top-level nodes for n in Page.objects.filter(parent__isnull=True).order_by('seqno'): #~ items += [li for li in n.get_sidebar_items(request,self)] items.append(n.get_sidebar_item(request, self)) if self.is_parented(n): children = [] for ch in n.children.order_by('seqno'): children.append(ch.get_sidebar_item(request, self)) if len(children): items.append(E.ul(*children, class_='nav nav-list')) e = E.ul(*items, class_='nav nav-list') return E.tostring_pretty(e) def get_sidebar_menu(self, request): #~ qs = self.get_siblings() qs = Page.objects.filter(parent__isnull=True) #~ qs = self.children.all() yield ('/', 'index', unicode(_('Home'))) #~ yield ('/downloads/', 'downloads', 'Downloads') #~ yield ('/about', 'about', 'About') #~ if qs is not None: for obj in qs: if obj.ref and obj.title: yield ('/' + obj.ref, obj.ref, dd.babelattr(obj, 'title'))
class Line(Referrable, Duplicable, ExcerptTitle, ContactRelated): class Meta: app_label = 'courses' abstract = dd.is_abstract_model(__name__, 'Line') verbose_name = pgettext("singular form", "Activity line") verbose_name_plural = pgettext("plural form", 'Activity lines') course_area = ActivityLayouts.field(default='default') # default=ActivityLayouts.get_lazy('default') topic = dd.ForeignKey(Topic, blank=True, null=True) description = dd.BabelTextField(_("Description"), blank=True) every_unit = Recurrencies.field(_("Recurrency"), default='weekly', blank=True) every = models.IntegerField(_("Repeat every"), default=1) event_type = dd.ForeignKey( 'cal.EventType', null=True, blank=True, help_text=_( "The type of calendar entries to be generated. " "If this is empty, no calendar entries will be generated.")) fee = dd.ForeignKey('products.Product', blank=True, null=True, verbose_name=_("Attendance fee"), related_name='lines_by_fee') guest_role = dd.ForeignKey( "cal.GuestRole", blank=True, null=True, verbose_name=_("Manage presences as"), help_text=_("The default guest role for particpants of " "calendar entries for activities in this series. " "Leave empty if you don't want any presences management.")) options_cat = dd.ForeignKey('products.ProductCat', verbose_name=_("Options category"), related_name="courses_lines_by_options_cat", blank=True, null=True) fees_cat = dd.ForeignKey('products.ProductCat', verbose_name=_("Fees category"), related_name="courses_lines_by_fees_cat", blank=True, null=True) body_template = models.CharField(max_length=200, verbose_name=_("Body template"), blank=True, help_text="The body template to use when " "printing a course of this series. " "Leave empty to use the site's default.") def __str__(self): name = dd.babelattr(self, 'name') if self.ref: return "{0} ({1})".format(self.ref, name) return name # return "{0} #{1}".format(self._meta.verbose_name, self.pk) @dd.chooser() def fee_choices(cls, fees_cat): Product = rt.models.products.Product if not fees_cat: return Product.objects.none() return Product.objects.filter(cat=fees_cat) @dd.chooser(simple_values=True) def body_template_choices(cls): return dd.plugins.jinja.list_templates( '.body.html', rt.models.courses.Enrolment.get_template_group(), 'excerpts')
class Course(Reservation, Duplicable, Printable): class Meta: app_label = 'courses' abstract = dd.is_abstract_model(__name__, 'Course') verbose_name = _("Activity") verbose_name_plural = _('Activities') # verbose_name = _("Event") # verbose_name_plural = _('Events') site_field_name = 'room' # line = dd.ForeignKey('courses.Line', null=True, blank=True) line = dd.ForeignKey('courses.Line') teacher = dd.ForeignKey(teacher_model, verbose_name=teacher_label, blank=True, null=True) #~ room = dd.ForeignKey(Room,blank=True,null=True) slot = dd.ForeignKey(Slot, blank=True, null=True) description = dd.BabelTextField(_("Description"), blank=True) remark = models.TextField(_("Remark"), blank=True) quick_search_fields = 'name line__name line__topic__name' state = CourseStates.field(default='draft') max_places = models.PositiveIntegerField( pgettext("in a course", "Available places"), help_text=("Maximum number of participants"), blank=True, null=True) name = models.CharField(_("Designation"), max_length=100, blank=True) enrolments_until = models.DateField(_("Enrolments until"), blank=True, null=True) print_presence_sheet = PrintPresenceSheet(show_in_bbar=False) print_presence_sheet_html = PrintPresenceSheet(show_in_bbar=False, build_method='weasy2html', label=format_lazy( u"{}{}", _("Presence sheet"), _(" (HTML)"))) @dd.displayfield(_("Print")) def print_actions(self, ar): if ar is None: return '' elems = [] elems.append(ar.instance_action_button(self.print_presence_sheet)) elems.append(ar.instance_action_button(self.print_presence_sheet_html)) return E.p(*join_elems(elems, sep=", ")) def on_duplicate(self, ar, master): self.state = CourseStates.draft super(Course, self).on_duplicate(ar, master) @classmethod def add_param_filter(cls, qs, lookup_prefix='', show_exposed=None, **kwargs): qs = super(Course, cls).add_param_filter(qs, **kwargs) exposed_states = CourseStates.filter(is_exposed=True) fkw = dict() fkw[lookup_prefix + 'state__in'] = exposed_states if show_exposed == dd.YesNo.no: qs = qs.exclude(**fkw) elif show_exposed == dd.YesNo.yes: qs = qs.filter(**fkw) return qs @classmethod def get_registrable_fields(cls, site): for f in super(Course, cls).get_registrable_fields(site): yield f yield 'line' yield 'teacher' yield 'name' yield 'enrolments_until' def __str__(self): if self.name: return self.name if self.line_id is None: line = self._meta.verbose_name else: line = self.line if self.room is None: return "%s (%s)" % (line, dd.fds(self.start_date)) return "%s (%s %s)" % (line, dd.fds(self.start_date), self.room) def get_detail_action(self, ar): """Custom :meth:`get_detail_action <lino.core.model.Model.get_detail_action>` because the detail_layout to use depends on the course's line's `course_area`. """ if self.line_id: area = self.line.course_area if area: table = rt.models.resolve(area.courses_table) ba = table.detail_action ba = ba.action.defining_actor.detail_action # if ar is None or ba.get_row_permission(ar, self, None): # return ba if ar is None or ba.get_view_permission( ar.get_user().user_type): return ba return None return super(Course, self).get_detail_action(ar) def update_cal_from(self, ar): """Note: if recurrency is weekly or per_weekday, actual start may be later than self.start_date """ # if self.state in (CourseStates.draft, CourseStates.cancelled): # if self.state == CourseStates.cancelled: # ar.info("No start date because state is %s", self.state) # return None return self.start_date def update_cal_event_type(self): if self.line_id: return self.line.event_type def update_cal_summary(self, et, i): if self.every_unit == Recurrencies.once: return self.name or str(self.line) return "%s %s" % (dd.babelattr(et, 'event_label'), i) def get_events_user(self): """The user of generated events is not the course manager (author) but the teacher. """ if self.teacher: return self.teacher.get_as_user() or self.user return self.user def suggest_cal_guests(self, event): """Look up enrolments of this course and suggest them as guests.""" # logger.info("20140314 suggest_guests") Enrolment = rt.models.courses.Enrolment # if self.line is None: # if self.line_id is None: # return # gr = self.line.guest_role or settings.SITE.pupil_guestrole # if gr is None: # return # fkw = dict(course=self) # states = (EnrolmentStates.requested, EnrolmentStates.confirmed) # fkw.update(state__in=states) qs = Enrolment.objects.filter(course=self).order_by( *[f.name for f in Enrolment.quick_search_fields]) for obj in qs: g = obj.make_guest_for(event) if g is not None: yield g def full_clean(self, *args, **kw): if self.line_id is not None: if self.id is None: descs = dd.field2kw(self.line, 'description') descs = dd.babelkw('description', **descs) for k, v in descs.items(): setattr(self, k, v) if self.every_unit is None: self.every_unit = self.line.every_unit if self.every is None: self.every = self.line.every # if self.room is None: # self.room = self.line.room # if self.enrolments_until is None: # self.enrolments_until = self.start_date # if self.id is not None: # if self.enrolments_until is None: # qs = self.get_existing_auto_events() # if qs.count(): # self.enrolments_until = qs[0].start_date super(Course, self).full_clean(*args, **kw) def before_auto_event_save(self, event): """ Set room and start_time/end_time for automatic events. """ assert not settings.SITE.loading_from_dump assert event.owner == self event.course = self event.room = self.room if self.slot: event.start_time = self.slot.start_time event.end_time = self.slot.end_time else: event.start_time = self.start_time event.end_time = self.end_time super(Course, self).before_auto_event_save(event) # @dd.displayfield(_("Info")) # def info(self, ar): # if ar is None: # return '' # return ar.obj2html(self) def get_overview_elems(self, ar): elems = [] # elems.append(ar.obj2html(self)) elems.append(self.obj2href(ar)) if self.teacher_id: elems.append(" / ") # elems.append(ar.obj2html(self.teacher)) elems.append(self.teacher.obj2href(ar)) return elems # @classmethod # def on_analyze(cls, site): # super(Course, cls).on_analyze(site) # dd.update_field(Course, 'detail_link', # default=site.models.courses.Course._meta.verbose_name) #~ @dd.displayfield(_("Where")) #~ def where_text(self,ar): # ~ return unicode(self.room) # .company.city or self.company) @dd.displayfield(_("Calendar entries")) def events_text(self, ar=None): if cal is not None: return ', '.join([ day_and_month(e.start_date) for e in self.events_by_course().order_by('start_date') ]) def events_by_course(self, **kwargs): ct = rt.models.contenttypes.ContentType.objects.get_for_model( self.__class__) kwargs.update(owner_type=ct, owner_id=self.id) return rt.models.cal.Event.objects.filter(**kwargs) def get_places_sum(self, today=None, **flt): Enrolment = rt.models.courses.Enrolment PeriodEvents = rt.models.system.PeriodEvents qs = Enrolment.objects.filter(course=self, **flt) # see voga.projects.roger.tests.test_max_places if today is None: rng = DateRangeValue(dd.today(), None) qs = PeriodEvents.active.add_filter(qs, rng) else: qs = PeriodEvents.active.add_filter(qs, today) # logger.info("20160502 %s", qs.query) res = qs.aggregate(models.Sum('places')) # logger.info("20140819 %s", res) return res['places__sum'] or 0 def get_free_places(self, today=None): if not self.max_places: return None # _("Unlimited") return self.max_places - self.get_used_places(today) def get_used_places(self, today=None): states = EnrolmentStates.filter(uses_a_place=True) return self.get_places_sum(today, state__in=states) # @dd.displayfield(_("Free places"), max_length=5) @dd.virtualfield(models.IntegerField(_("Free places"))) def free_places(self, ar=None): # if not self.max_places: # return None # _("Unlimited") return self.get_free_places() @dd.virtualfield(models.IntegerField(_("Requested"))) def requested(self, ar): return self.get_places_sum(state=EnrolmentStates.requested) # pv = dict(start_date=dd.today()) # pv.update(state=EnrolmentStates.requested) # return rt.models.courses.EnrolmentsByCourse.request( # self, param_values=pv) @dd.virtualfield(models.IntegerField(_("Confirmed"))) def confirmed(self, ar): return self.get_places_sum(state=EnrolmentStates.confirmed) # pv = dict(start_date=dd.today()) # pv.update(state=EnrolmentStates.confirmed) # return rt.models.courses.EnrolmentsByCourse.request( # self, param_values=pv) @dd.virtualfield(models.IntegerField(_("Trying"))) def trying(self, ar): return self.get_places_sum(state=EnrolmentStates.trying) @dd.requestfield(_("Enrolments")) def enrolments(self, ar): return self.get_enrolments(start_date=dd.today()) def get_enrolments(self, **pv): # pv = dict(start_date=sd, end_date=dd.today()) return rt.models.courses.EnrolmentsByCourse.request(self, param_values=pv)
class PaymentTerm(mixins.BabelNamed, mixins.Referrable): """A convention on how an invoice should be paid. The following fields define the default value for `due_date`: .. attribute:: days Number of days to add to :attr:`voucher_date`. .. attribute:: months Number of months to add to :attr:`voucher_date`. .. attribute:: end_of_month Whether to move :attr:`voucher_date` to the end of month. .. attribute:: printed_text Used in :xfile:`sales/VatProductInvoice/trailer.html` as follows:: {% if obj.payment_term.printed_text %} {{parse(obj.payment_term.printed_text)}} {% else %} {{_("Payment terms")}} : {{obj.payment_term}} {% endif %} The :attr:`printed_text` field is important when using **prepayments** or other more complex payment terms. Lino uses a rather simple approach to handle prepayment invoices: only the global amount and the final due date is stored in the database, all intermediate amounts and due dates are just generated in the printable document. You just define one :class:`PaymentTerm <lino_cosi.lib.ledger.models.PaymentTerm>` row for each prepayment formula and configure your :attr:`printed_text` field. For example:: Prepayment <b>30%</b> ({{(obj.total_incl*30)/100}} {{obj.currency}}) due on <b>{{fds(obj.due_date)}}</b>, remaining {{obj.total_incl - (obj.total_incl*30)/100}} {{obj.currency}} due 10 days before delivery. """ class Meta: app_label = 'ledger' verbose_name = _("Payment Term") verbose_name_plural = _("Payment Terms") days = models.IntegerField(_("Days"), default=0) months = models.IntegerField(_("Months"), default=0) end_of_month = models.BooleanField(_("End of month"), default=False) printed_text = dd.BabelTextField(_("Printed text"), blank=True, format='plain') def get_due_date(self, date1): assert isinstance(date1, datetime.date), \ "%s is not a date" % date1 d = date1 + relativedelta(months=self.months, days=self.days) if self.end_of_month: d = last_day_of_month(d) return d
class Product(mixins.BabelNamed, Duplicable): class Meta: app_label = 'products' verbose_name = _("Product") verbose_name_plural = _("Products") abstract = dd.is_abstract_model(__name__, 'Product') description = dd.BabelTextField( verbose_name=_("Long description"), blank=True, null=True) cat = dd.ForeignKey( ProductCat, verbose_name=_("Category"), blank=True, null=True) delivery_unit = DeliveryUnits.field(default='piece') product_type = ProductTypes.field() vat_class = VatClasses.field(blank=True) @dd.chooser() def cat_choices(self, product_type): qs = rt.models.products.ProductCats.request().data_iterator if product_type is not None: qs = qs.filter(product_type=product_type) return qs @classmethod def get_product_choices(cls, partner): """Return a list of products (fees) that are allowed for the specified partner. """ Product = cls qs = Product.objects.filter(product_type=ProductTypes.default) qs = qs.order_by('name') rules = PriceRule.objects.all() for pf in PriceFactors.get_list_items(): rules = rules.filter( Q(**{pf.field_name: getattr(partner, pf.field_name)}) | Q(**{pf.field_name + '__isnull': True})) return [p for p in qs if rules.filter(product=p).count() > 0] # TODO: add rules condition as subquery to qs and return the query, not # the list @classmethod def get_rule_fee(cls, partner, event_type): if partner is None: return for rule in rt.models.products.PriceRule.objects.order_by('seqno'): ok = True for pf in PriceFactors.get_list_items(): rv = getattr(rule, pf.field_name) if rv: pv = getattr(partner, pf.field_name) if pv != rv: # print("20181128a {} != {}".format(rv, pv)) ok = False # if rule.tariff and rule.tariff != tariff: # # print("20181128b {} != {}".format(rule.tariff, tariff)) # ok = False if rule.event_type and rule.event_type != event_type: # print("20181128c {} != {}".format(rule.event_type, event_type)) ok = False if ok and rule.fee is not None: return rule.fee def full_clean(self): # print("20191210", self.name, self.vat_class) if self.product_type is None: if self.cat_id: self.product_type = self.cat.product_type or ProductTypes.default else: self.product_type = ProductTypes.default super(Product, self).full_clean()