class CustomerProducerInvoice(models.Model): customer = models.ForeignKey( 'Customer', verbose_name=_("Customer"), on_delete=models.PROTECT) producer = models.ForeignKey( 'Producer', verbose_name=_("Producer"), on_delete=models.PROTECT) permanence = models.ForeignKey( 'Permanence', verbose_name=_('Order'), on_delete=models.PROTECT, db_index=True) # Calculated with Purchase total_purchase_with_tax = ModelMoneyField( _("Producer amount invoiced"), help_text=_('Total selling amount vat included'), default=DECIMAL_ZERO, max_digits=8, decimal_places=2) # Calculated with Purchase total_selling_with_tax = ModelMoneyField( _("Invoiced to the consumer including tax"), help_text=_('Total selling amount vat included'), default=DECIMAL_ZERO, max_digits=8, decimal_places=2) def get_html_producer_price_purchased(self): if self.total_purchase_with_tax != DECIMAL_ZERO: return mark_safe(_("<b>%(price)s</b>") % {'price': self.total_purchase_with_tax}) return EMPTY_STRING get_html_producer_price_purchased.short_description = (_("Producer amount invoiced")) get_html_producer_price_purchased.allow_tags = True get_html_producer_price_purchased.admin_order_field = 'total_purchase_with_tax' def __str__(self): return "{}, {}".format(self.producer, self.customer) class Meta: unique_together = ("permanence", "customer", "producer",)
class LUT_DeliveryPoint(MPTTModel, TranslatableModel): parent = TreeForeignKey('self', null=True, blank=True, related_name='children') translations = TranslatedFields( short_name=models.CharField(_("Short name"), max_length=50, default=EMPTY_STRING), description=HTMLField(_("Description"), configuration='CKEDITOR_SETTINGS_MODEL2', blank=True, default=EMPTY_STRING), ) is_active = models.BooleanField(_("Active"), default=True) customer_responsible = models.ForeignKey( 'Customer', verbose_name=_("Customer responsible"), help_text=_("Invoices are sent to this customer who is responsible for collecting the payments."), blank=True, null=True, default=None) inform_customer_responsible = models.BooleanField(_("Inform the group of orders placed by its members"), default=False) transport = ModelMoneyField( _("Delivery point shipping cost"), # help_text=_("This amount is added once for groups with entitled customer or at each customer for open groups."), default=DECIMAL_ZERO, blank=True, max_digits=5, decimal_places=2, validators=[MinValueValidator(0)]) min_transport = ModelMoneyField( _("Minium order amount for free shipping cost"), # help_text=_("This is the minimum order amount to avoid shipping cost."), default=DECIMAL_ZERO, blank=True, max_digits=5, decimal_places=2, validators=[MinValueValidator(0)]) objects = LUT_DeliveryPointManager() def __str__(self): if self.customer_responsible is not None: return "[{}]".format(self.customer_responsible.short_basket_name) else: return self.safe_translation_getter('short_name', any_language=True, default=EMPTY_STRING) class Meta: verbose_name = _("Delivery point") verbose_name_plural = _("Deliveries points")
class Invoice(models.Model): permanence = models.ForeignKey( 'Permanence', verbose_name=_('Order'), on_delete=models.PROTECT, db_index=True) status = models.CharField( max_length=3, choices=LUT_PERMANENCE_STATUS, default=PERMANENCE_PLANNED, verbose_name=_("Status")) date_previous_balance = models.DateField( _("Date previous balance"), default=datetime.date.today) previous_balance = ModelMoneyField( _("Previous balance"), max_digits=8, decimal_places=2, default=DECIMAL_ZERO) # Calculated with Purchase total_price_with_tax = ModelMoneyField( _("Invoiced TVAC"), default=DECIMAL_ZERO, max_digits=8, decimal_places=2) delta_price_with_tax = ModelMoneyField( _("Total amount"), help_text=_('Purchase to add amount vat included'), default=DECIMAL_ZERO, max_digits=8, decimal_places=2) delta_transport = ModelMoneyField( _("Delivery point shipping cost"), help_text=_("Transport to add"), default=DECIMAL_ZERO, max_digits=5, decimal_places=2, validators=[MinValueValidator(0)]) total_vat = ModelMoneyField( _("VAT"), default=DECIMAL_ZERO, max_digits=9, decimal_places=4) delta_vat = ModelMoneyField( _("VAT to add"), default=DECIMAL_ZERO, max_digits=9, decimal_places=4) total_deposit = ModelMoneyField( _("Deposit"), help_text=_('Surcharge'), default=DECIMAL_ZERO, max_digits=8, decimal_places=2) bank_amount_in = ModelMoneyField( _("Cash in"), help_text=_('Payment on the account'), max_digits=8, decimal_places=2, default=DECIMAL_ZERO) bank_amount_out = ModelMoneyField( _("Cash out"), help_text=_('Payment from the account'), max_digits=8, decimal_places=2, default=DECIMAL_ZERO) date_balance = models.DateField( _("Date balance"), default=datetime.date.today) balance = ModelMoneyField( _("Balance"), max_digits=8, decimal_places=2, default=DECIMAL_ZERO) def get_delta_price_with_tax(self): return self.delta_price_with_tax.amount def get_abs_delta_price_with_tax(self): return abs(self.delta_price_with_tax.amount) def __str__(self): return _("Invoice") class Meta: abstract = True
class BoxContent(models.Model): box = models.ForeignKey( "Box", verbose_name=_("Box"), null=True, blank=True, db_index=True, on_delete=models.PROTECT, ) product = models.ForeignKey( "Product", verbose_name=_("Product"), related_name="box_content", null=True, blank=True, db_index=True, on_delete=models.PROTECT, ) content_quantity = models.DecimalField( _("Fixed quantity per unit"), default=DECIMAL_ZERO, max_digits=6, decimal_places=3, validators=[MinValueValidator(0)], ) calculated_customer_content_price = ModelMoneyField( _("Calculated consumer tariff"), default=DECIMAL_ZERO, max_digits=8, decimal_places=2, ) calculated_content_deposit = ModelMoneyField( _("Content deposit"), help_text=_("Surcharge"), default=DECIMAL_ZERO, max_digits=8, decimal_places=2, ) def get_calculated_customer_content_price(self): # workaround for a display problem with Money field in the admin list_display return self.calculated_customer_content_price + self.calculated_content_deposit get_calculated_customer_content_price.short_description = _( "Calculated consumer tariff") def __str__(self): return EMPTY_STRING class Meta: verbose_name = _("Box content") verbose_name_plural = _("Boxes content") unique_together = (("box", "product"), ) index_together = [["product", "box"]]
class BoxContent(models.Model): box = models.ForeignKey('Box', verbose_name=_("box"), null=True, blank=True, db_index=True, on_delete=models.PROTECT) product = models.ForeignKey('Product', verbose_name=_("product"), related_name='box_content', null=True, blank=True, db_index=True, on_delete=models.PROTECT) content_quantity = models.DecimalField(_("fixed content quantity"), default=DECIMAL_ZERO, max_digits=6, decimal_places=3, validators=[MinValueValidator(0)]) calculated_customer_content_price = ModelMoneyField( _("customer content price"), default=DECIMAL_ZERO, max_digits=8, decimal_places=2) calculated_content_deposit = ModelMoneyField( _("content deposit"), help_text=_('deposit to add to the original content price'), default=DECIMAL_ZERO, max_digits=8, decimal_places=2) def get_calculated_customer_content_price(self): # workaround for a display problem with Money field in the admin list_display return self.calculated_customer_content_price + self.calculated_content_deposit get_calculated_customer_content_price.short_description = ( _("customer content price")) get_calculated_customer_content_price.allow_tags = False def __str__(self): return EMPTY_STRING class Meta: verbose_name = _("box content") verbose_name_plural = _("boxes content") unique_together = ( "box", "product", ) index_together = [ ["product", "box"], ]
class CustomerProducerInvoice(models.Model): customer = models.ForeignKey( "Customer", verbose_name=_("Customer"), on_delete=models.PROTECT ) producer = models.ForeignKey( "Producer", verbose_name=_("Producer"), on_delete=models.PROTECT ) permanence = models.ForeignKey( "Permanence", verbose_name=_("Order"), on_delete=models.PROTECT, db_index=True ) # Calculated with Purchase total_purchase_with_tax = ModelMoneyField( _("Producer amount invoiced"), help_text=_("Total selling amount vat included"), default=DECIMAL_ZERO, max_digits=8, decimal_places=2, ) # Calculated with Purchase total_selling_with_tax = ModelMoneyField( _("Invoiced to the consumer w TVA"), help_text=_("Total selling amount vat included"), default=DECIMAL_ZERO, max_digits=8, decimal_places=2, ) def get_html_producer_price_purchased(self): if self.total_purchase_with_tax != DECIMAL_ZERO: return format_html("<b>{}</b>", self.total_purchase_with_tax) return EMPTY_STRING get_html_producer_price_purchased.short_description = _("Producer amount invoiced") get_html_producer_price_purchased.admin_order_field = "total_purchase_with_tax" def __str__(self): return "{}, {}".format(self.producer, self.customer) class Meta: unique_together = (("permanence", "customer", "producer"),)
class Producer(models.Model): short_profile_name = models.CharField(_("Short name"), max_length=25, null=False, default=EMPTY_STRING, db_index=True, unique=True) long_profile_name = models.CharField(_("Long name"), max_length=100, null=True, default=EMPTY_STRING) email = models.EmailField(_("Email"), null=True, blank=True, default=EMPTY_STRING) email2 = models.EmailField(_("Secondary email"), null=True, blank=True, default=EMPTY_STRING) email3 = models.EmailField(_("Secondary email"), null=True, blank=True, default=EMPTY_STRING) language = models.CharField(max_length=5, choices=settings.LANGUAGES, default=settings.LANGUAGE_CODE, verbose_name=_("Language")) picture = AjaxPictureField(verbose_name=_("Picture"), null=True, blank=True, upload_to="producer", size=SIZE_L) phone1 = models.CharField(_("Phone1"), max_length=25, null=True, blank=True, default=EMPTY_STRING) phone2 = models.CharField(_("Phone2"), max_length=25, null=True, blank=True, default=EMPTY_STRING) bank_account = models.CharField(_("Bank account"), max_length=100, null=True, blank=True, default=EMPTY_STRING) vat_id = models.CharField(_("VAT id"), max_length=20, null=True, blank=True, default=EMPTY_STRING) fax = models.CharField(_("Fax"), max_length=100, null=True, blank=True, default=EMPTY_STRING) address = models.TextField(_("Address"), null=True, blank=True, default=EMPTY_STRING) city = models.CharField(_("City"), max_length=50, null=True, blank=True, default=EMPTY_STRING) memo = models.TextField(_("Memo"), null=True, blank=True, default=EMPTY_STRING) reference_site = models.URLField(_("Reference site"), null=True, blank=True, default=EMPTY_STRING) web_services_activated = models.BooleanField(_('Web services activated'), default=False) # uuid used to access to producer invoices without login uuid = models.CharField("uuid", max_length=36, null=True, default=EMPTY_STRING, db_index=True) offer_uuid = models.CharField("uuid", max_length=36, null=True, default=EMPTY_STRING, db_index=True) offer_filled = models.BooleanField(_("Offer filled"), default=False) invoice_by_basket = models.BooleanField(_("Invoice by basket"), default=False) manage_replenishment = models.BooleanField(_("Manage replenishment"), default=False) producer_pre_opening = models.BooleanField(_("Pre-open the orders"), default=False) producer_price_are_wo_vat = models.BooleanField( _("Producer price are wo vat"), default=False) sort_products_by_reference = models.BooleanField( _("Sort products by reference"), default=False) price_list_multiplier = models.DecimalField( _("Coefficient applied to the producer tariff to calculate the consumer tariff" ), help_text= _("This multiplier is applied to each price automatically imported/pushed." ), default=DECIMAL_ONE, max_digits=5, decimal_places=4, blank=True, validators=[MinValueValidator(0)]) is_resale_price_fixed = models.BooleanField( _("The resale price is set by the producer"), default=False) minimum_order_value = ModelMoneyField( _("Minimum order value"), help_text=_("0 mean : no minimum order value."), max_digits=8, decimal_places=2, default=DECIMAL_ZERO, validators=[MinValueValidator(0)]) date_balance = models.DateField(_("Date_balance"), default=datetime.date.today) balance = ModelMoneyField(_("Balance"), max_digits=8, decimal_places=2, default=DECIMAL_ZERO) initial_balance = ModelMoneyField(_("Initial balance"), max_digits=8, decimal_places=2, default=DECIMAL_ZERO) represent_this_buyinggroup = models.BooleanField( _("Represent this buyinggroup"), default=False) is_active = models.BooleanField(_("Active"), default=True) # This indicate that the user record data have been replaced with anonymous data in application of GDPR is_anonymized = models.BooleanField(default=False) @classmethod def get_or_create_group(cls): producer_buyinggroup = Producer.objects.filter( represent_this_buyinggroup=True).order_by('?').first() if producer_buyinggroup is None: long_name = settings.REPANIER_SETTINGS_GROUP_NAME short_name = long_name[:25] producer_buyinggroup = Producer.objects.create( short_profile_name=short_name, long_profile_name=long_name, phone1=settings.REPANIER_SETTINGS_COORDINATOR_PHONE, represent_this_buyinggroup=True) # Create this to also prevent the deletion of the producer representing the buying group membership_fee_product = Product.objects.filter( order_unit=PRODUCT_ORDER_UNIT_MEMBERSHIP_FEE, is_active=True).order_by('?') if not membership_fee_product.exists(): membership_fee_product = Product.objects.create( producer_id=producer_buyinggroup.id, order_unit=PRODUCT_ORDER_UNIT_MEMBERSHIP_FEE, vat_level=VAT_100) cur_language = translation.get_language() for language in settings.PARLER_LANGUAGES[settings.SITE_ID]: language_code = language["code"] translation.activate(language_code) membership_fee_product.set_current_language(language_code) membership_fee_product.long_name = "{}".format( _("Membership fee")) membership_fee_product.save() translation.activate(cur_language) return producer_buyinggroup def get_negative_balance(self): return -self.balance def get_products(self): # This producer may have product's list if self.is_active: changeproductslist_url = urlresolvers.reverse( 'admin:repanier_product_changelist', ) link = "<a href=\"{}?is_active__exact=1&producer={}\" class=\"btn addlink\"> {}</a>".format( changeproductslist_url, str(self.id), _("Products")) return link return EMPTY_STRING get_products.short_description = (_("Link to his products")) get_products.allow_tags = True def get_admin_date_balance(self): if self.id is not None: bank_account = BankAccount.objects.filter( producer_id=self.id, producer_invoice__isnull=True).order_by( "-operation_date").only("operation_date").first() if bank_account is not None: return bank_account.operation_date return self.date_balance else: return timezone.now().date() get_admin_date_balance.short_description = (_("Date_balance")) get_admin_date_balance.allow_tags = False def get_admin_balance(self): if self.id is not None: return self.balance - self.get_bank_not_invoiced( ) + self.get_order_not_invoiced() else: return REPANIER_MONEY_ZERO get_admin_balance.short_description = (_("Balance")) get_admin_balance.allow_tags = False def get_order_not_invoiced(self): if settings.REPANIER_SETTINGS_MANAGE_ACCOUNTING: result_set = ProducerInvoice.objects.filter( producer_id=self.id, status__gte=PERMANENCE_OPENED, status__lte=PERMANENCE_SEND).order_by('?').aggregate( Sum('total_price_with_tax'), Sum('delta_price_with_tax'), Sum('delta_transport')) if result_set["total_price_with_tax__sum"] is not None: order_not_invoiced = RepanierMoney( result_set["total_price_with_tax__sum"]) else: order_not_invoiced = REPANIER_MONEY_ZERO if result_set["delta_price_with_tax__sum"] is not None: order_not_invoiced += RepanierMoney( result_set["delta_price_with_tax__sum"]) if result_set["delta_transport__sum"] is not None: order_not_invoiced += RepanierMoney( result_set["delta_transport__sum"]) else: order_not_invoiced = REPANIER_MONEY_ZERO return order_not_invoiced def get_bank_not_invoiced(self): if settings.REPANIER_SETTINGS_MANAGE_ACCOUNTING: result_set = BankAccount.objects.filter( producer_id=self.id, producer_invoice__isnull=True).order_by('?').aggregate( Sum('bank_amount_in'), Sum('bank_amount_out')) if result_set["bank_amount_in__sum"] is not None: bank_in = RepanierMoney(result_set["bank_amount_in__sum"]) else: bank_in = REPANIER_MONEY_ZERO if result_set["bank_amount_out__sum"] is not None: bank_out = RepanierMoney(result_set["bank_amount_out__sum"]) else: bank_out = REPANIER_MONEY_ZERO bank_not_invoiced = bank_in - bank_out else: bank_not_invoiced = REPANIER_MONEY_ZERO return bank_not_invoiced def get_calculated_invoiced_balance(self, permanence_id): bank_not_invoiced = self.get_bank_not_invoiced() # IMPORTANT : when is_resale_price_fixed=True then price_list_multiplier == 1 # Do not take into account product whose order unit is >= PRODUCT_ORDER_UNIT_DEPOSIT result_set = OfferItemWoReceiver.objects.filter( permanence_id=permanence_id, producer_id=self.id, price_list_multiplier__lt=1).exclude( order_unit__gte=PRODUCT_ORDER_UNIT_DEPOSIT).order_by( '?').aggregate(Sum('total_selling_with_tax')) if result_set["total_selling_with_tax__sum"] is not None: payment_needed = result_set["total_selling_with_tax__sum"] else: payment_needed = DECIMAL_ZERO result_set = OfferItemWoReceiver.objects.filter( permanence_id=permanence_id, producer_id=self.id, price_list_multiplier__gte=1, ).exclude(order_unit__gte=PRODUCT_ORDER_UNIT_DEPOSIT).order_by( '?').aggregate(Sum('total_purchase_with_tax'), ) if result_set["total_purchase_with_tax__sum"] is not None: payment_needed += result_set["total_purchase_with_tax__sum"] calculated_invoiced_balance = self.balance - bank_not_invoiced + payment_needed if self.manage_replenishment: for offer_item in OfferItemWoReceiver.objects.filter( is_active=True, permanence_id=permanence_id, producer_id=self.id, manage_replenishment=True, ).order_by('?'): invoiced_qty, taken_from_stock, customer_qty = offer_item.get_producer_qty_stock_invoiced( ) if offer_item.price_list_multiplier < DECIMAL_ONE: # or offer_item.is_resale_price_fixed: unit_price = offer_item.customer_unit_price.amount else: unit_price = offer_item.producer_unit_price.amount if taken_from_stock > DECIMAL_ZERO: delta_price_with_tax = ( (unit_price + offer_item.unit_deposit.amount) * taken_from_stock).quantize(TWO_DECIMALS) calculated_invoiced_balance -= delta_price_with_tax return calculated_invoiced_balance get_calculated_invoiced_balance.short_description = (_("Balance")) get_calculated_invoiced_balance.allow_tags = False def get_balance(self): last_producer_invoice_set = ProducerInvoice.objects.filter( producer_id=self.id, invoice_sort_order__isnull=False).order_by('?') balance = self.get_admin_balance() if last_producer_invoice_set.exists(): if balance.amount < 0: return '<a href="' + urlresolvers.reverse( 'producer_invoice_view', args=(0, )) + '?producer=' + str( self.id) + '" class="btn" target="_blank" >' + ( "<span style=\"color:#298A08\">{}</span>".format( -balance)) + '</a>' elif balance.amount == 0: return '<a href="' + urlresolvers.reverse( 'producer_invoice_view', args=(0, )) + '?producer=' + str( self.id) + '" class="btn" target="_blank" >' + ( "<span style=\"color:#32CD32\">{}</span>".format( -balance)) + '</a>' elif balance.amount > 30: return '<a href="' + urlresolvers.reverse( 'producer_invoice_view', args=(0, )) + '?producer=' + str( self.id) + '" class="btn" target="_blank" >' + ( "<span style=\"color:red\">{}</span>".format( -balance)) + '</a>' else: return '<a href="' + urlresolvers.reverse( 'producer_invoice_view', args=(0, )) + '?producer=' + str( self.id) + '" class="btn" target="_blank" >' + ( "<span style=\"color:#696969\">{}</span>".format( -balance)) + '</a>' else: if balance.amount < 0: return "<span style=\"color:#298A08\">{}</span>".format( -balance) elif balance.amount == 0: return "<span style=\"color:#32CD32\">{}</span>".format( -balance) elif balance.amount > 30: return "<span style=\"color:red\">{}</span>".format(-balance) else: return "<span style=\"color:#696969\">{}</span>".format( -balance) get_balance.short_description = _("Balance") get_balance.allow_tags = True get_balance.admin_order_field = 'balance' def get_last_invoice(self): producer_last_invoice = ProducerInvoice.objects.filter( producer_id=self.id, invoice_sort_order__isnull=False).order_by("-id").first() if producer_last_invoice is not None: if producer_last_invoice.total_price_with_tax < DECIMAL_ZERO: return "<span style=\"color:#298A08\">{}</span>".format( number_format(producer_last_invoice.total_price_with_tax, 2)) elif producer_last_invoice.total_price_with_tax == DECIMAL_ZERO: return "<span style=\"color:#32CD32\">{}</span>".format( number_format(producer_last_invoice.total_price_with_tax, 2)) elif producer_last_invoice.total_price_with_tax > 30: return "<span style=\"color:red\">{}</span>".format( number_format(producer_last_invoice.total_price_with_tax, 2)) else: return "<span style=\"color:#696969\">{}</span>".format( number_format(producer_last_invoice.total_price_with_tax, 2)) else: return "<span style=\"color:#32CD32\">{}</span>".format( number_format(0, 2)) get_last_invoice.short_description = _("Last invoice") get_last_invoice.allow_tags = True def get_html_on_hold_movement(self): bank_not_invoiced = self.get_bank_not_invoiced() order_not_invoiced = self.get_order_not_invoiced() if order_not_invoiced.amount != DECIMAL_ZERO or bank_not_invoiced.amount != DECIMAL_ZERO: if order_not_invoiced.amount != DECIMAL_ZERO: if bank_not_invoiced.amount == DECIMAL_ZERO: producer_on_hold_movement = \ _('This balance does not take account of any unbilled sales %(other_order)s.') % { 'other_order': order_not_invoiced } else: producer_on_hold_movement = \ _( 'This balance does not take account of any unrecognized payments %(bank)s and any unbilled order %(other_order)s.') \ % { 'bank': bank_not_invoiced, 'other_order': order_not_invoiced } else: producer_on_hold_movement = \ _( 'This balance does not take account of any unrecognized payments %(bank)s.') % { 'bank': bank_not_invoiced } return mark_safe(producer_on_hold_movement) return EMPTY_STRING def anonymize(self, also_group=False): if self.represent_this_buyinggroup: if not also_group: return self.short_profile_name = "{}-{}".format(_("GROUP"), self.id) self.long_profile_name = "{} {}".format(_("Group"), self.id) else: self.short_profile_name = "{}-{}".format(_("PRODUCER"), self.id) self.long_profile_name = "{} {}".format(_("Producer"), self.id) self.email = "{}@repanier.be".format(self.short_profile_name) self.email2 = EMPTY_STRING self.email3 = EMPTY_STRING self.phone1 = EMPTY_STRING self.phone2 = EMPTY_STRING self.bank_account = EMPTY_STRING self.vat_id = EMPTY_STRING self.fax = EMPTY_STRING self.address = EMPTY_STRING self.memo = EMPTY_STRING self.uuid = uuid.uuid1() self.offer_uuid = uuid.uuid1() self.is_anonymized = True self.save() def __str__(self): if self.producer_price_are_wo_vat: return "{} {}".format(self.short_profile_name, _("wo tax")) return self.short_profile_name class Meta: verbose_name = _("Producer") verbose_name_plural = _("Producers") ordering = ( "-represent_this_buyinggroup", "short_profile_name", ) indexes = [ models.Index( fields=["-represent_this_buyinggroup", "short_profile_name"], name='producer_order_idx'), ]
class CustomerInvoice(Invoice): customer = models.ForeignKey( 'Customer', verbose_name=_("customer"), on_delete=models.PROTECT) customer_charged = models.ForeignKey( 'Customer', verbose_name=_("customer"), related_name='invoices_paid', blank=True, null=True, on_delete=models.PROTECT, db_index=True) delivery = models.ForeignKey( 'DeliveryBoard', verbose_name=_("delivery board"), null=True, blank=True, default=None, on_delete=models.PROTECT) # IMPORTANT: default = True -> for the order form, to display nothing at the begin of the order # is_order_confirm_send and total_price_with_tax = 0 --> display nothing # otherwise display # - send a mail with the order to me # - confirm the order (if REPANIER_SETTINGS_CUSTOMERS_MUST_CONFIRM_ORDERS) and send a mail with the order to me # - mail send to XYZ # - order confirmed (if REPANIER_SETTINGS_CUSTOMERS_MUST_CONFIRM_ORDERS) and mail send to XYZ is_order_confirm_send = models.BooleanField(_("is_order_confirm_send"), choices=LUT_CONFIRM, default=False) invoice_sort_order = models.IntegerField( _("invoice sort order"), default=None, blank=True, null=True, db_index=True) price_list_multiplier = models.DecimalField( _("Delivery point price list multiplier"), help_text=_("This multiplier is applied once for groups with entitled customer or at each customer invoice for open groups."), default=DECIMAL_ONE, max_digits=5, decimal_places=4, blank=True, validators=[MinValueValidator(0)]) transport = ModelMoneyField( _("Delivery point shipping cost"), help_text=_("This amount is added once for groups with entitled customer or at each customer for open groups."), default=DECIMAL_ZERO, max_digits=5, decimal_places=2, validators=[MinValueValidator(0)]) min_transport = ModelMoneyField( _("Minium order amount for free shipping cost"), help_text=_("This is the minimum order amount to avoid shipping cost."), default=DECIMAL_ZERO, max_digits=5, decimal_places=2, validators=[MinValueValidator(0)]) master_permanence = models.ForeignKey( 'Permanence', verbose_name=_("master permanence"), related_name='child_customer_invoice', blank=True, null=True, default=None, on_delete=models.PROTECT, db_index=True) is_group = models.BooleanField(_("is a group"), default=False) def get_abs_delta_vat(self): return abs(self.delta_vat) def get_total_price_with_tax(self, customer_charged=False): if self.customer_id == self.customer_charged_id: return self.total_price_with_tax + self.delta_price_with_tax + self.delta_transport else: if self.status < PERMANENCE_INVOICED or not customer_charged: return self.total_price_with_tax else: return self.customer_charged # if self.total_price_with_tax != DECIMAL_ZERO else RepanierMoney() def get_total_price_wo_tax(self): return self.get_total_price_with_tax() - self.get_total_tax() def get_total_tax(self): # round to 2 decimals return RepanierMoney(self.total_vat.amount + self.delta_vat.amount) @transaction.atomic def set_delivery(self, delivery): # May not use delivery_id because it won't reload customer_invoice.delivery # Important # If it's an invoice of a member of a group : # self.customer_charged_id != self.customer_id # self.customer_charged_id == owner of the group # price_list_multiplier = DECIMAL_ONE # Else : # self.customer_charged_id = self.customer_id # price_list_multiplier may vary from repanier.apps import REPANIER_SETTINGS_TRANSPORT, REPANIER_SETTINGS_MIN_TRANSPORT if delivery is None: if self.permanence.with_delivery_point: # If the customer is member of a group set the group as default delivery point delivery_point = self.customer.delivery_point delivery = DeliveryBoard.objects.filter( delivery_point=delivery_point, permanence=self.permanence ).order_by('?').first() else: delivery_point = None else: delivery_point = delivery.delivery_point self.delivery = delivery if delivery_point is None: self.customer_charged = self.customer self.price_list_multiplier = DECIMAL_ONE self.transport = REPANIER_SETTINGS_TRANSPORT self.min_transport = REPANIER_SETTINGS_MIN_TRANSPORT else: customer_responsible = delivery_point.customer_responsible if customer_responsible is None: self.customer_charged = self.customer self.price_list_multiplier = DECIMAL_ONE self.transport = delivery_point.transport self.min_transport = delivery_point.min_transport else: self.customer_charged = customer_responsible self.price_list_multiplier = DECIMAL_ONE self.transport = REPANIER_MONEY_ZERO self.min_transport = REPANIER_MONEY_ZERO if self.customer_id != customer_responsible.id: customer_invoice_charged = CustomerInvoice.objects.filter( permanence_id=self.permanence_id, customer_id=customer_responsible.id ).order_by('?') if not customer_invoice_charged.exists(): CustomerInvoice.objects.create( permanence_id=self.permanence_id, customer_id=customer_responsible.id, status=self.status, customer_charged_id=customer_responsible.id, price_list_multiplier=customer_responsible.price_list_multiplier, transport=delivery_point.transport, min_transport=delivery_point.min_transport, is_order_confirm_send=True, is_group=True, delivery=delivery ) def my_order_confirmation(self, permanence, is_basket=False, basket_message=EMPTY_STRING, to_json=None): from repanier.apps import REPANIER_SETTINGS_CUSTOMERS_MUST_CONFIRM_ORDERS if permanence.with_delivery_point: if self.delivery is not None: label = self.delivery.get_delivery_customer_display() delivery_id = self.delivery_id else: delivery_id = 0 if self.customer.delivery_point is not None: qs = DeliveryBoard.objects.filter( Q( permanence_id=permanence.id, delivery_point_id=self.customer.delivery_point_id, status=PERMANENCE_OPENED ) | Q( permanence_id=permanence.id, delivery_point__customer_responsible__isnull=False, status=PERMANENCE_OPENED ) ).order_by('?') else: qs = DeliveryBoard.objects.filter( permanence_id=permanence.id, delivery_point__customer_responsible__isnull=False, status=PERMANENCE_OPENED ).order_by('?') if qs.exists(): label = "%s" % _('Please, select a delivery point') CustomerInvoice.objects.filter( permanence_id=permanence.id, customer_id=self.customer_id).order_by('?').update( status=PERMANENCE_OPENED) else: label = "%s" % _('No delivery point is open for you. You can not place order.') # IMPORTANT : # 1 / This prohibit to place an order into the customer UI # 2 / task_order.close_send_order will delete any CLOSED orders without any delivery point CustomerInvoice.objects.filter( permanence_id=permanence.id, customer_id=self.customer_id ).order_by('?').update( status=PERMANENCE_CLOSED) if self.customer_id != self.customer_charged_id: msg_price = msg_transport = EMPTY_STRING else: if self.transport.amount <= DECIMAL_ZERO: transport = False msg_transport = EMPTY_STRING else: transport = True if self.min_transport.amount > DECIMAL_ZERO: msg_transport = "%s<br/>" % \ _( 'The shipping costs for this delivery point amount to %(transport)s for orders of less than %(min_transport)s.') % { 'transport' : self.transport, 'min_transport': self.min_transport } else: msg_transport = "%s<br/>" % \ _( 'The shipping costs for this delivery point amount to %(transport)s.') % { 'transport': self.transport, } if self.price_list_multiplier == DECIMAL_ONE: msg_price = EMPTY_STRING else: if transport: if self.price_list_multiplier > DECIMAL_ONE: msg_price = "%s<br/>" % \ _( 'A price increase of %(increase)s %% of the total invoiced is due for this delivery point. This does not apply to the cost of transport which is fixed.') % { 'increase': number_format( (self.price_list_multiplier - DECIMAL_ONE) * 100, 2) } else: msg_price = "%s<br/>" % \ _( 'A price decrease of %(decrease)s %% of the total invoiced is given for this delivery point. This does not apply to the cost of transport which is fixed.') % { 'decrease': number_format( (DECIMAL_ONE - self.price_list_multiplier) * 100, 2) } else: if self.price_list_multiplier > DECIMAL_ONE: msg_price = "%s<br/>" % \ _( 'A price increase of %(increase)s %% of the total invoiced is due for this delivery point.') % { 'increase': number_format( (self.price_list_multiplier - DECIMAL_ONE) * 100, 2) } else: msg_price = "%s<br/>" % \ _( 'A price decrease of %(decrease)s %% of the total invoiced is given for this delivery point.') % { 'decrease': number_format( (DECIMAL_ONE - self.price_list_multiplier) * 100, 2) } msg_delivery = '%s<b><i><select name="delivery" id="delivery" onmouseover="show_select_delivery_list_ajax(%d)" onchange="delivery_ajax(%d)" class="form-control"><option value="%d" selected>%s</option></select></i></b><br/>%s%s' % ( _("Delivery point"), delivery_id, delivery_id, delivery_id, label, msg_transport, msg_price ) else: msg_delivery = EMPTY_STRING msg_confirmation1 = EMPTY_STRING if not is_basket and not REPANIER_SETTINGS_CUSTOMERS_MUST_CONFIRM_ORDERS: # or customer_invoice.total_price_with_tax.amount != DECIMAL_ZERO: # If apps.REPANIER_SETTINGS_CUSTOMERS_MUST_CONFIRM_ORDERS is True, # then permanence.with_delivery_point is also True msg_html = EMPTY_STRING else: if self.is_order_confirm_send: msg_confirmation2 = self.customer.my_order_confirmation_email_send_to() msg_html = """ <div class="row"> <div class="panel panel-default"> <div class="panel-heading"> %s <p><font color="#51a351">%s</font><p/> %s </div> </div> </div> """ % (msg_delivery, msg_confirmation2, basket_message) else: msg_html = None btn_disabled = EMPTY_STRING if permanence.status == PERMANENCE_OPENED else "disabled" msg_confirmation2 = EMPTY_STRING if REPANIER_SETTINGS_CUSTOMERS_MUST_CONFIRM_ORDERS: if is_basket: if self.status == PERMANENCE_OPENED: if (permanence.with_delivery_point and self.delivery is None) \ or self.total_price_with_tax == DECIMAL_ZERO: btn_disabled = "disabled" msg_confirmation1 = '<font color="red">%s</font><br/>' % _( "An unconfirmed order will be canceled.") msg_confirmation2 = '<span class="glyphicon glyphicon-floppy-disk"></span> %s' % _( "Confirm this order and receive an email containing its summary.") else: href = urlresolvers.reverse( 'order_view', args=(permanence.id,) ) if self.status == PERMANENCE_OPENED: msg_confirmation1 = '<font color="red">%s</font><br/>' % _( "An unconfirmed order will be canceled.") msg_confirmation2 = _("Verify my order content before validating it.") msg_html = """ <div class="row"> <div class="panel panel-default"> <div class="panel-heading"> %s %s <a href="%s?is_basket=yes" class="btn btn-info" %s>%s</a> </div> </div> </div> """ % (msg_delivery, msg_confirmation1, href, btn_disabled, msg_confirmation2) else: if is_basket: msg_confirmation2 = _("Receive an email containing this order summary.") elif permanence.with_delivery_point: msg_html = """ <div class="row"> <div class="panel panel-default"> <div class="panel-heading"> %s </div> </div> </div> """ % msg_delivery else: msg_html = EMPTY_STRING if msg_html is None: if msg_confirmation2 == EMPTY_STRING: msg_html = """ <div class="row"> <div class="panel panel-default"> <div class="panel-heading"> %s <div class="clearfix"></div> %s </div> </div> </div> """ % (msg_delivery, basket_message) else: msg_html = """ <div class="row"> <div class="panel panel-default"> <div class="panel-heading"> %s %s <button id="btn_confirm_order" class="btn btn-info" %s onclick="btn_receive_order_email();">%s</button> <div class="clearfix"></div> %s </div> </div> </div> """ % (msg_delivery, msg_confirmation1, btn_disabled, msg_confirmation2, basket_message) if to_json is not None: option_dict = {'id': "#span_btn_confirm_order", 'html': msg_html} to_json.append(option_dict) @transaction.atomic def confirm_order(self): from repanier.models.purchase import Purchase Purchase.objects.filter( customer_invoice__id=self.id ).update(quantity_confirmed=F('quantity_ordered')) self.calculate_and_save_delta_buyinggroup(confirm_order=True) self.is_order_confirm_send = True def calculate_and_save_delta_buyinggroup(self, confirm_order=False): previous_delta_price_with_tax = self.delta_price_with_tax.amount previous_delta_vat = self.delta_vat.amount previous_delta_transport = self.delta_transport.amount self.calculate_delta_price(confirm_order) self.calculate_delta_transport() if previous_delta_price_with_tax != self.delta_price_with_tax.amount or previous_delta_vat != self.delta_vat.amount or previous_delta_transport != self.delta_transport.amount: producer_invoice_buyinggroup = ProducerInvoice.objects.filter( producer__represent_this_buyinggroup=True, permanence_id=self.permanence_id, ).order_by('?').first() if producer_invoice_buyinggroup is None: # producer_buyinggroup = Producer.objects.filter( # represent_this_buyinggroup=True # ).order_by('?').first() producer_invoice_buyinggroup = ProducerInvoice.objects.create( producer_id=REPANIER_SETTINGS_GROUP_PRODUCER_ID, permanence_id=self.permanence_id, status=self.permanence.status ) producer_invoice_buyinggroup.delta_price_with_tax.amount += self.delta_price_with_tax.amount - previous_delta_price_with_tax producer_invoice_buyinggroup.delta_vat.amount += self.delta_vat.amount - previous_delta_vat producer_invoice_buyinggroup.delta_transport.amount += self.delta_transport.amount - previous_delta_transport producer_invoice_buyinggroup.save() def calculate_delta_price(self, confirm_order=False): from repanier.models.purchase import Purchase getcontext().rounding = ROUND_HALF_UP self.delta_price_with_tax.amount = DECIMAL_ZERO self.delta_vat.amount = DECIMAL_ZERO # Important # Si c'est une facture du membre d'un groupe : # self.customer_charged_id == purchase.customer_charged_id != self.customer_id # self.customer_charged_id == purchase.customer_charged_id == owner of the group # self.price_list_multiplier = DECIMAL_ONE # Si c'est une facture lambda ou d'un groupe : # self.customer_charged_id == purchase.customer_charged_id = self.customer_id # self.price_list_multiplier may vary if self.customer_id == self.customer_charged_id: if self.price_list_multiplier != DECIMAL_ONE: result_set = Purchase.objects.filter( permanence_id=self.permanence_id, customer_invoice__customer_charged_id=self.customer_id, is_resale_price_fixed=False ).order_by('?').aggregate( Sum('customer_vat'), Sum('deposit'), Sum('selling_price') ) if result_set["customer_vat__sum"] is not None: total_vat = result_set["customer_vat__sum"] else: total_vat = DECIMAL_ZERO if result_set["deposit__sum"] is not None: total_deposit = result_set["deposit__sum"] else: total_deposit = DECIMAL_ZERO if result_set["selling_price__sum"] is not None: total_price_with_tax = result_set["selling_price__sum"] else: total_price_with_tax = DECIMAL_ZERO total_price_with_tax_wo_deposit = total_price_with_tax - total_deposit self.delta_price_with_tax.amount = -( total_price_with_tax_wo_deposit - ( total_price_with_tax_wo_deposit * self.price_list_multiplier ).quantize(TWO_DECIMALS) ) self.delta_vat.amount = -( total_vat - ( total_vat * self.price_list_multiplier ).quantize(FOUR_DECIMALS) ) result_set = Purchase.objects.filter( permanence_id=self.permanence_id, customer_invoice__customer_charged_id=self.customer_id, ).order_by('?').aggregate( Sum('customer_vat'), Sum('deposit'), Sum('selling_price') ) else: result_set = Purchase.objects.filter( permanence_id=self.permanence_id, customer_id=self.customer_id, ).order_by('?').aggregate( Sum('customer_vat'), Sum('deposit'), Sum('selling_price') ) if result_set["customer_vat__sum"] is not None: self.total_vat.amount = result_set["customer_vat__sum"] else: self.total_vat.amount = DECIMAL_ZERO if result_set["deposit__sum"] is not None: self.total_deposit.amount = result_set["deposit__sum"] else: self.total_deposit.amount = DECIMAL_ZERO if result_set["selling_price__sum"] is not None: self.total_price_with_tax.amount = result_set["selling_price__sum"] else: self.total_price_with_tax.amount = DECIMAL_ZERO def calculate_delta_transport(self): self.delta_transport.amount = DECIMAL_ZERO if self.master_permanence_id is None and self.transport.amount != DECIMAL_ZERO: # Calculate transport only on master customer invoice # But take into account the children customer invoices result_set = CustomerInvoice.objects.filter( master_permanence_id=self.permanence_id ).order_by('?').aggregate( Sum('total_price_with_tax'), Sum('delta_price_with_tax') ) if result_set["total_price_with_tax__sum"] is not None: sum_total_price_with_tax = result_set["total_price_with_tax__sum"] else: sum_total_price_with_tax = DECIMAL_ZERO if result_set["delta_price_with_tax__sum"] is not None: sum_delta_price_with_tax = result_set["delta_price_with_tax__sum"] else: sum_delta_price_with_tax = DECIMAL_ZERO sum_total_price_with_tax += self.total_price_with_tax.amount sum_delta_price_with_tax += self.delta_price_with_tax.amount total_price_with_tax = sum_total_price_with_tax + sum_delta_price_with_tax if total_price_with_tax != DECIMAL_ZERO: if self.min_transport.amount == DECIMAL_ZERO: self.delta_transport.amount = self.transport.amount elif total_price_with_tax < self.min_transport.amount: self.delta_transport.amount = min( self.min_transport.amount - total_price_with_tax, self.transport.amount ) def cancel_confirm_order(self): if self.is_order_confirm_send: # Change of confirmation status self.is_order_confirm_send = False return True else: # No change of confirmation status return False def create_child(self, new_permanence): if self.customer_id != self.customer_charged_id: # TODO : Créer la customer invoice du groupe customer_invoice = CustomerInvoice.objects.filter( permanence_id=self.permanence_id, customer_id=self.customer_charged_id ).only("id").order_by('?') if not customer_invoice.exists(): customer_invoice = CustomerInvoice.objects.create( permanence_id=self.permanence_id, customer_id=self.customer_charged_id, customer_charged_id=self.customer_charged_id, status=self.status ) customer_invoice.set_delivery(delivery=None) customer_invoice.save() return CustomerInvoice.objects.create( permanence_id=new_permanence.id, customer_id=self.customer_id, master_permanence_id=self.permanence_id, customer_charged_id=self.customer_charged_id, status=self.status ) def delete_if_unconfirmed(self, permanence): if not self.is_order_confirm_send: from repanier.email.email_order import export_order_2_1_customer from repanier.models.purchase import Purchase filename = "{0}-{1}.xlsx".format( slugify(_("Canceled order")), slugify(permanence) ) sender_email, sender_function, signature, cc_email_staff = get_signature( is_reply_to_order_email=True) export_order_2_1_customer( self.customer, filename, permanence, sender_email, sender_function, signature, cancel_order=True ) purchase_qs = Purchase.objects.filter( customer_invoice_id=self.id, is_box_content=False, ).order_by('?') for a_purchase in purchase_qs.select_related("customer"): create_or_update_one_cart_item( customer=a_purchase.customer, offer_item_id=a_purchase.offer_item_id, q_order=DECIMAL_ZERO, batch_job=True, comment=_("Cancelled qty : %s") % number_format(a_purchase.quantity_ordered, 4) ) def __str__(self): return '%s, %s' % (self.customer, self.permanence) class Meta: verbose_name = _("customer invoice") verbose_name_plural = _("customers invoices") unique_together = ("permanence", "customer",)
class ProducerInvoice(Invoice): producer = models.ForeignKey( 'Producer', verbose_name=_("producer"), # related_name='producer_invoice', on_delete=models.PROTECT) delta_stock_with_tax = ModelMoneyField( _("Total stock"), help_text=_('stock taken amount vat included'), default=DECIMAL_ZERO, max_digits=8, decimal_places=2) delta_stock_vat = ModelMoneyField( _("Total stock vat"), help_text=_('vat to add'), default=DECIMAL_ZERO, max_digits=9, decimal_places=4) delta_deposit = ModelMoneyField( _("deposit"), help_text=_('deposit to add'), default=DECIMAL_ZERO, max_digits=8, decimal_places=2) delta_stock_deposit = ModelMoneyField( _("deposit"), help_text=_('deposit to add'), default=DECIMAL_ZERO, max_digits=8, decimal_places=2) to_be_paid = models.BooleanField(_("to be paid"), choices=LUT_BANK_NOTE, default=False) calculated_invoiced_balance = ModelMoneyField( _("calculated balance to be invoiced"), max_digits=8, decimal_places=2, default=DECIMAL_ZERO) to_be_invoiced_balance = ModelMoneyField( _("balance to be invoiced"), max_digits=8, decimal_places=2, default=DECIMAL_ZERO) invoice_sort_order = models.IntegerField( _("invoice sort order"), default=None, blank=True, null=True, db_index=True) invoice_reference = models.CharField( _("invoice reference"), max_length=100, null=True, blank=True) def get_negative_previous_balance(self): return - self.previous_balance def get_negative_balance(self): return - self.balance def get_total_price_with_tax(self): return self.total_price_with_tax + self.delta_price_with_tax + self.delta_transport + self.delta_stock_with_tax def get_total_vat(self): return self.total_vat + self.delta_stock_vat def get_total_deposit(self): return self.total_deposit + self.delta_stock_deposit def get_order_json(self, to_json): a_producer = self.producer if a_producer.minimum_order_value.amount > DECIMAL_ZERO: ratio = self.total_price_with_tax.amount / a_producer.minimum_order_value.amount if ratio >= DECIMAL_ONE: ratio = 100 else: ratio *= 100 option_dict = {'id' : "#order_procent%d" % a_producer.id, 'html': "%s%%" % number_format(ratio, 0)} to_json.append(option_dict) if self.status != PERMANENCE_OPENED: option_dict = {'id' : "#order_closed%d" % a_producer.id, 'html': ' <span class="glyphicon glyphicon-ban-circle" aria-hidden="true"></span>'} to_json.append(option_dict) return def __str__(self): return '%s, %s' % (self.producer, self.permanence) class Meta: verbose_name = _("producer invoice") verbose_name_plural = _("producers invoices") unique_together = ("permanence", "producer",)
class Invoice(models.Model): permanence = models.ForeignKey( 'Permanence', verbose_name=permanence_verbose_name(), on_delete=models.PROTECT, db_index=True) status = models.CharField( max_length=3, choices=LUT_PERMANENCE_STATUS, default=PERMANENCE_PLANNED, verbose_name=_("invoice_status")) date_previous_balance = models.DateField( _("date_previous_balance"), default=datetime.date.today) previous_balance = ModelMoneyField( _("previous_balance"), max_digits=8, decimal_places=2, default=DECIMAL_ZERO) # Calculated with Purchase total_price_with_tax = ModelMoneyField( _("Total amount"), help_text=_('Total purchase amount vat included'), default=DECIMAL_ZERO, max_digits=8, decimal_places=2) delta_price_with_tax = ModelMoneyField( _("Total amount"), help_text=_('purchase to add amount vat included'), default=DECIMAL_ZERO, max_digits=8, decimal_places=2) delta_transport = ModelMoneyField( _("Delivery point transport"), help_text=_("transport to add"), default=DECIMAL_ZERO, max_digits=5, decimal_places=2, validators=[MinValueValidator(0)]) total_vat = ModelMoneyField( _("Total vat"), help_text=_('Vat part of the total purchased'), default=DECIMAL_ZERO, max_digits=9, decimal_places=4) delta_vat = ModelMoneyField( _("Total vat"), help_text=_('vat to add'), default=DECIMAL_ZERO, max_digits=9, decimal_places=4) total_deposit = ModelMoneyField( _("deposit"), help_text=_('deposit to add to the original unit price'), default=DECIMAL_ZERO, max_digits=8, decimal_places=2) bank_amount_in = ModelMoneyField( _("bank_amount_in"), help_text=_('payment_on_the_account'), max_digits=8, decimal_places=2, default=DECIMAL_ZERO) bank_amount_out = ModelMoneyField( _("bank_amount_out"), help_text=_('payment_from_the_account'), max_digits=8, decimal_places=2, default=DECIMAL_ZERO) date_balance = models.DateField( _("date_balance"), default=datetime.date.today) balance = ModelMoneyField( _("balance"), max_digits=8, decimal_places=2, default=DECIMAL_ZERO) def get_delta_price_with_tax(self): return self.delta_price_with_tax.amount def get_abs_delta_price_with_tax(self): return abs(self.delta_price_with_tax.amount) def __str__(self): return _("Invoice") class Meta: abstract = True
class Configuration(TranslatableModel): group_name = models.CharField(_("group name"), max_length=50, default=EMPTY_STRING) test_mode = models.BooleanField(_("test mode"), default=False) login_attempt_counter = models.DecimalField(_("login attempt counter"), default=DECIMAL_ZERO, max_digits=2, decimal_places=0) password_reset_on = models.DateTimeField(_("password_reset_on"), null=True, blank=True, default=None) name = models.CharField(max_length=3, choices=LUT_PERMANENCE_NAME, default=PERMANENCE_NAME_PERMANENCE, verbose_name=_("offers name")) currency = models.CharField(max_length=3, choices=LUT_CURRENCY, default=CURRENCY_EUR, verbose_name=_("currency")) max_week_wo_participation = models.DecimalField( _("display a pop up on the order form after this max week wo participation" ), help_text=_("0 mean : never display a pop up."), default=DECIMAL_ZERO, max_digits=2, decimal_places=0, validators=[MinValueValidator(0)]) send_opening_mail_to_customer = models.BooleanField( _("send opening mail to customers"), default=True) send_abstract_order_mail_to_customer = models.BooleanField( _("send abstract order mail to customers"), default=False) send_order_mail_to_customer = models.BooleanField( _("send order mail to customers"), default=True) send_cancel_order_mail_to_customer = models.BooleanField( _("send cancel order mail to customers"), default=True) send_abstract_order_mail_to_producer = models.BooleanField( _("send abstract order mail to producers"), default=False) send_order_mail_to_producer = models.BooleanField( _("send order mail to producers"), default=True) send_order_mail_to_board = models.BooleanField( _("send order mail to board"), default=True) send_invoice_mail_to_customer = models.BooleanField( _("send invoice mail to customers"), default=True) send_invoice_mail_to_producer = models.BooleanField( _("send invoice mail to producers"), default=False) invoice = models.BooleanField(_("activate invoice"), default=True) close_wo_sending = models.BooleanField(_("close wo sending"), default=False) display_anonymous_order_form = models.BooleanField( _("display anonymous order form"), default=True) display_producer_on_order_form = models.BooleanField( _("display producers on order form"), default=True) display_who_is_who = models.BooleanField(_("display who is who"), default=True) bank_account = models.CharField(_("bank account"), max_length=100, null=True, blank=True, default=EMPTY_STRING) vat_id = models.CharField(_("vat_id"), max_length=20, null=True, blank=True, default=EMPTY_STRING) page_break_on_customer_check = models.BooleanField( _("page break on customer check"), default=False) sms_gateway_mail = models.EmailField( _("sms gateway email"), help_text= _("To actually send sms, use for e.g. on a GSM : https://play.google.com/store/apps/details?id=eu.apksoft.android.smsgateway" ), max_length=50, null=True, blank=True, default=EMPTY_STRING) customers_must_confirm_orders = models.BooleanField( _("customers must confirm orders"), default=False) membership_fee = ModelMoneyField(_("membership fee"), default=DECIMAL_ZERO, max_digits=8, decimal_places=2) membership_fee_duration = models.DecimalField( _("membership fee duration"), help_text=_("number of month(s). 0 mean : no membership fee."), default=DECIMAL_ZERO, max_digits=3, decimal_places=0, validators=[MinValueValidator(0)]) home_site = models.URLField(_("home site"), null=True, blank=True, default=EMPTY_STRING) permanence_of_last_cancelled_invoice = models.ForeignKey( 'Permanence', on_delete=models.PROTECT, blank=True, null=True) transport = ModelMoneyField( _("Shipping cost"), help_text=_("This amount is added to order less than min_transport."), default=DECIMAL_ZERO, max_digits=5, decimal_places=2, validators=[MinValueValidator(0)]) min_transport = ModelMoneyField( _("Minium order amount for free shipping cost"), help_text=_( "This is the minimum order amount to avoid shipping cost."), default=DECIMAL_ZERO, max_digits=5, decimal_places=2, validators=[MinValueValidator(0)]) notification_is_public = models.BooleanField( _("the notification is public"), default=False) email_is_custom = models.BooleanField(_("Email is customised"), default=False) email_host = models.CharField( _("email host"), help_text= _("For @gmail.com, see: https://mail.google.com/mail/u/0/#settings/fwdandpop and activate POP" ), max_length=50, null=True, blank=True, default="smtp.gmail.com") email_port = models.IntegerField( _("email port"), help_text=_("Usually 587 for @gmail.com, otherwise 25"), blank=True, null=True, default=587) email_use_tls = models.BooleanField( _("email use tls"), help_text=_("TLS is used otherwise SSL is used"), default=True) email_host_user = models.EmailField( _("email host user"), help_text=_("For @gmail.com : [email protected]"), max_length=50, null=True, blank=True, default="*****@*****.**") email_host_password = models.CharField( _("email host password"), help_text= _("For @gmail.com, you must generate an application password, see: https://security.google.com/settings/security/apppasswords" ), max_length=25, null=True, blank=True, default=EMPTY_STRING) translations = TranslatedFields( group_label=models.CharField(_("group label"), max_length=100, default=EMPTY_STRING, blank=True), how_to_register=HTMLField(_("how to register"), help_text=EMPTY_STRING, configuration='CKEDITOR_SETTINGS_MODEL2', default=EMPTY_STRING, blank=True), notification=HTMLField(_("notification"), help_text=EMPTY_STRING, configuration='CKEDITOR_SETTINGS_MODEL2', default=EMPTY_STRING, blank=True), offer_customer_mail=HTMLField(_("offer customer mail"), help_text=EMPTY_STRING, configuration='CKEDITOR_SETTINGS_MODEL2', default=""" Bonjour,<br /> <br /> Les commandes de la {{ permanence_link }} sont maintenant ouvertes auprès de : {{ offer_producer }}.<br /> {% if offer_description %}{{ offer_description }}<br /> {% endif %} {% if offer_recent_detail %}<br />Nouveauté(s) :<br /> {{ offer_recent_detail }}{% endif %}<br /> <br /> {{ signature }} """, blank=True), offer_producer_mail=HTMLField(_("offer producer mail"), help_text=EMPTY_STRING, configuration='CKEDITOR_SETTINGS_MODEL2', default=""" Cher/Chère {{ long_profile_name }},<br /> <br /> {% if offer_description != "" %}Voici l'annonce consommateur :<br /> {{ offer_description }}<br /> <br /> {% endif %} Veuillez vérifier votre <strong>{{ offer_link }}</strong>.<br /> <br /> {{ signature }} """, blank=True), order_customer_mail=HTMLField(_("order customer mail"), help_text=EMPTY_STRING, configuration='CKEDITOR_SETTINGS_MODEL2', default=""" Bonjour {{ long_basket_name }},<br /> <br /> En pièce jointe vous trouverez le montant de votre panier {{ short_basket_name }} de la {{ permanence_link }}.<br /> <br /> {{ last_balance }}<br /> {{ order_amount }}<br /> {% if on_hold_movement %}{{ on_hold_movement }}<br /> {% endif %} {% if payment_needed %}{{ payment_needed }}<br /> {% endif %}<br /> <br /> {{ signature }} """, blank=True), cancel_order_customer_mail=HTMLField( _("cancelled order customer mail"), help_text=EMPTY_STRING, configuration='CKEDITOR_SETTINGS_MODEL2', default=""" Bonjour {{ long_basket_name }},<br /> <br /> La commande ci-jointe de votre panier {{ short_basket_name }} de la {{ permanence_link }} <b>a été annulée</b> car vous ne l'avez pas confirmée.<br /> <br /> {{ signature }} """, blank=True), order_staff_mail=HTMLField(_("order staff mail"), help_text=EMPTY_STRING, configuration='CKEDITOR_SETTINGS_MODEL2', default=""" Cher/Chère membre de l'équipe de préparation,<br/> <br/> En pièce jointe vous trouverez la liste de préparation pour la {{ permanence_link }}.<br/> <br/> L'équipe de préparation est composée de :<br/> {{ board_composition }}<br/> ou de<br/> {{ board_composition_and_description }}<br/> <br/> {{ signature }} """, blank=True), order_producer_mail=HTMLField(_("order producer mail"), help_text=EMPTY_STRING, configuration='CKEDITOR_SETTINGS_MODEL2', default=""" Cher/Chère {{ name }},<br /> <br /> {% if order_empty %}Le groupe ne vous a rien acheté pour la {{ permanence_link }}.{% else %}En pièce jointe, vous trouverez la commande du groupe pour la {{ permanence }}.{% if duplicate %}<br /> <strong>ATTENTION </strong>: La commande est présente en deux exemplaires. Le premier exemplaire est classé par produit et le duplicata est classé par panier.{% else %}{% endif %}{% endif %}<br /> <br /> {{ signature }} """, blank=True), invoice_customer_mail=HTMLField( _("invoice customer mail"), help_text=EMPTY_STRING, configuration='CKEDITOR_SETTINGS_MODEL2', default=""" Bonjour {{ name }},<br/> <br/> En cliquant sur ce lien vous trouverez votre facture pour la {{ permanence_link }}.{% if invoice_description %}<br/> <br/> {{ invoice_description }}{% endif %} <br /> {{ order_amount }}<br /> {{ last_balance_link }}<br /> {% if payment_needed %}{{ payment_needed }}<br /> {% endif %}<br /> <br /> {{ signature }} """, blank=True), invoice_producer_mail=HTMLField( _("invoice producer mail"), help_text=EMPTY_STRING, configuration='CKEDITOR_SETTINGS_MODEL2', default=""" Cher/Chère {{ profile_name }},<br/> <br/> En cliquant sur ce lien vous trouverez le détail de notre paiement pour la {{ permanence_link }}.<br/> <br/> {{ signature }} """, blank=True), ) def clean(self): try: template = Template(self.offer_customer_mail) except Exception as error_str: raise ValidationError( mark_safe("%s : %s" % (self.offer_customer_mail, error_str))) try: template = Template(self.offer_producer_mail) except Exception as error_str: raise ValidationError( mark_safe("%s : %s" % (self.offer_producer_mail, error_str))) try: template = Template(self.order_customer_mail) except Exception as error_str: raise ValidationError( mark_safe("%s : %s" % (self.order_customer_mail, error_str))) try: template = Template(self.order_staff_mail) except Exception as error_str: raise ValidationError( mark_safe("%s : %s" % (self.order_staff_mail, error_str))) try: template = Template(self.order_producer_mail) except Exception as error_str: raise ValidationError( mark_safe("%s : %s" % (self.order_producer_mail, error_str))) try: template = Template(self.invoice_customer_mail) except Exception as error_str: raise ValidationError( mark_safe("%s : %s" % (self.invoice_customer_mail, error_str))) try: template = Template(self.invoice_producer_mail) except Exception as error_str: raise ValidationError( mark_safe("%s : %s" % (self.invoice_producer_mail, error_str))) def __str__(self): return EMPTY_STRING class Meta: verbose_name = _("configuration") verbose_name_plural = _("configurations")
class Item(TranslatableModel): producer = models.ForeignKey('Producer', verbose_name=_("producer"), on_delete=models.PROTECT) department_for_customer = models.ForeignKey( 'LUT_DepartmentForCustomer', verbose_name=_("department_for_customer"), blank=True, null=True, on_delete=models.PROTECT) picture2 = AjaxPictureField(verbose_name=_("picture"), null=True, blank=True, upload_to="product", size=SIZE_L) reference = models.CharField(_("reference"), max_length=36, blank=True, null=True) order_unit = models.CharField( max_length=3, choices=LUT_PRODUCT_ORDER_UNIT, default=PRODUCT_ORDER_UNIT_PC, verbose_name=_("order unit"), ) order_average_weight = models.DecimalField( _("order_average_weight"), help_text=_( 'if useful, average order weight (eg : 0,1 Kg [i.e. 100 gr], 3 Kg)' ), default=DECIMAL_ZERO, max_digits=6, decimal_places=3, validators=[MinValueValidator(0)]) producer_unit_price = ModelMoneyField(_("producer unit price"), default=DECIMAL_ZERO, max_digits=8, decimal_places=2) customer_unit_price = ModelMoneyField(_("customer unit price"), default=DECIMAL_ZERO, max_digits=8, decimal_places=2) producer_vat = ModelMoneyField(_("vat"), default=DECIMAL_ZERO, max_digits=8, decimal_places=4) customer_vat = ModelMoneyField(_("vat"), default=DECIMAL_ZERO, max_digits=8, decimal_places=4) unit_deposit = ModelMoneyField( _("deposit"), help_text=_('deposit to add to the original unit price'), default=DECIMAL_ZERO, max_digits=8, decimal_places=2, validators=[MinValueValidator(0)]) vat_level = models.CharField( max_length=3, choices=LUT_ALL_VAT, # settings.LUT_VAT, default=settings.DICT_VAT_DEFAULT, verbose_name=_("tax")) wrapped = models.BooleanField(_('Individually wrapped by the producer'), default=False) customer_minimum_order_quantity = models.DecimalField( _("customer_minimum_order_quantity"), help_text=_( 'minimum order qty (eg : 0,1 Kg [i.e. 100 gr], 1 piece, 3 Kg)'), default=DECIMAL_ONE, max_digits=6, decimal_places=3, validators=[MinValueValidator(0)]) customer_increment_order_quantity = models.DecimalField( _("customer_increment_order_quantity"), help_text=_( 'increment order qty (eg : 0,05 Kg [i.e. 50max 1 piece, 3 Kg)'), default=DECIMAL_ONE, max_digits=6, decimal_places=3, validators=[MinValueValidator(0)]) customer_alert_order_quantity = models.DecimalField( _("customer_alert_order_quantity"), help_text= _('maximum order qty before alerting the customer to check (eg : 1,5 Kg, 12 pieces, 9 Kg)' ), default=LIMIT_ORDER_QTY_ITEM, max_digits=6, decimal_places=3, validators=[MinValueValidator(0)]) producer_order_by_quantity = models.DecimalField( _("Producer order by quantity"), help_text=_('1,5 Kg [i.e. 1500 gr], 1 piece, 3 Kg)'), default=DECIMAL_ZERO, max_digits=6, decimal_places=3, validators=[MinValueValidator(0)]) placement = models.CharField( max_length=3, choices=LUT_PRODUCT_PLACEMENT, default=PRODUCT_PLACEMENT_BASKET, verbose_name=_("product_placement"), help_text= _('used for helping to determine the order of preparation of this product' )) stock = models.DecimalField(_("Current stock"), default=DECIMAL_MAX_STOCK, max_digits=9, decimal_places=3, validators=[MinValueValidator(0)]) limit_order_quantity_to_stock = models.BooleanField( _("limit maximum order qty of the group to stock qty"), default=False) is_box = models.BooleanField(_("is a box"), default=False) # is_membership_fee = models.BooleanField(_("is_membership_fee"), default=False) # may_order = models.BooleanField(_("may_order"), default=True) is_active = models.BooleanField(_("is_active"), default=True) @property def producer_unit_price_wo_tax(self): if self.producer_price_are_wo_vat: return self.producer_unit_price else: return self.producer_unit_price - self.producer_vat @property def email_offer_price_with_vat(self): offer_price = self.get_reference_price() if offer_price == EMPTY_STRING: offer_price = self.get_unit_price() return offer_price def set_from(self, source): self.is_active = source.is_active self.picture2 = source.picture2 self.reference = source.reference self.department_for_customer_id = source.department_for_customer_id self.producer_id = source.producer_id self.order_unit = source.order_unit self.wrapped = source.wrapped self.order_average_weight = source.order_average_weight self.placement = source.placement self.vat_level = source.vat_level self.customer_unit_price = source.customer_unit_price self.customer_vat = source.customer_vat self.producer_unit_price = source.producer_unit_price self.producer_vat = source.producer_vat self.unit_deposit = source.unit_deposit self.limit_order_quantity_to_stock = source.limit_order_quantity_to_stock self.stock = source.stock self.customer_minimum_order_quantity = source.customer_minimum_order_quantity self.customer_increment_order_quantity = source.customer_increment_order_quantity self.customer_alert_order_quantity = source.customer_alert_order_quantity self.producer_order_by_quantity = source.producer_order_by_quantity self.is_box = source.is_box def recalculate_prices(self, producer_price_are_wo_vat, is_resale_price_fixed, price_list_multiplier): getcontext().rounding = ROUND_HALF_UP vat = DICT_VAT[self.vat_level] vat_rate = vat[DICT_VAT_RATE] if producer_price_are_wo_vat: self.producer_vat.amount = (self.producer_unit_price.amount * vat_rate).quantize(FOUR_DECIMALS) if not is_resale_price_fixed: if self.order_unit < PRODUCT_ORDER_UNIT_DEPOSIT: self.customer_unit_price.amount = ( self.producer_unit_price.amount * price_list_multiplier).quantize(TWO_DECIMALS) else: self.customer_unit_price = self.producer_unit_price self.customer_vat.amount = (self.customer_unit_price.amount * vat_rate).quantize(FOUR_DECIMALS) if not is_resale_price_fixed: self.customer_unit_price += self.customer_vat else: self.producer_vat.amount = self.producer_unit_price.amount - ( self.producer_unit_price.amount / (DECIMAL_ONE + vat_rate)).quantize(FOUR_DECIMALS) if not is_resale_price_fixed: if self.order_unit < PRODUCT_ORDER_UNIT_DEPOSIT: self.customer_unit_price.amount = ( self.producer_unit_price.amount * price_list_multiplier).quantize(TWO_DECIMALS) else: self.customer_unit_price = self.producer_unit_price self.customer_vat.amount = self.customer_unit_price.amount - ( self.customer_unit_price.amount / (DECIMAL_ONE + vat_rate)).quantize(FOUR_DECIMALS) def get_unit_price(self, customer_price=True): if customer_price: unit_price = self.customer_unit_price else: unit_price = self.producer_unit_price if self.order_unit in [ PRODUCT_ORDER_UNIT_KG, PRODUCT_ORDER_UNIT_PC_KG ]: return "%s %s" % (unit_price, _("/ kg")) elif self.order_unit == PRODUCT_ORDER_UNIT_LT: return "%s %s" % (unit_price, _("/ l")) elif self.order_unit not in [ PRODUCT_ORDER_UNIT_PC_PRICE_KG, PRODUCT_ORDER_UNIT_PC_PRICE_LT, PRODUCT_ORDER_UNIT_PC_PRICE_PC ]: return "%s %s" % (unit_price, _("/ piece")) else: return "%s" % (unit_price, ) def get_reference_price(self, customer_price=True): if self.order_average_weight > DECIMAL_ZERO and self.order_average_weight != DECIMAL_ONE: if self.order_unit in [ PRODUCT_ORDER_UNIT_PC_PRICE_KG, PRODUCT_ORDER_UNIT_PC_PRICE_LT, PRODUCT_ORDER_UNIT_PC_PRICE_PC ]: if customer_price: reference_price = self.customer_unit_price.amount / self.order_average_weight else: reference_price = self.producer_unit_price.amount / self.order_average_weight reference_price = RepanierMoney( reference_price.quantize(TWO_DECIMALS), 2) if self.order_unit == PRODUCT_ORDER_UNIT_PC_PRICE_KG: reference_unit = _("/ kg") elif self.order_unit == PRODUCT_ORDER_UNIT_PC_PRICE_LT: reference_unit = _("/ l") else: reference_unit = _("/ pc") return "%s %s" % (reference_price, reference_unit) else: return EMPTY_STRING else: return EMPTY_STRING def get_display(self, qty=0, order_unit=PRODUCT_ORDER_UNIT_PC, unit_price_amount=None, for_customer=True, for_order_select=False, without_price_display=False): magnitude = None display_qty = True if order_unit == PRODUCT_ORDER_UNIT_KG: if qty == DECIMAL_ZERO: unit = EMPTY_STRING elif for_customer and qty < 1: unit = "%s" % (_('gr')) magnitude = 1000 else: unit = "%s" % (_('kg')) elif order_unit == PRODUCT_ORDER_UNIT_LT: if qty == DECIMAL_ZERO: unit = EMPTY_STRING elif for_customer and qty < 1: unit = "%s" % (_('cl')) magnitude = 100 else: unit = "%s" % (_('l')) elif order_unit in [ PRODUCT_ORDER_UNIT_PC_KG, PRODUCT_ORDER_UNIT_PC_PRICE_KG ]: # display_qty = not (order_average_weight == 1 and order_unit == PRODUCT_ORDER_UNIT_PC_PRICE_KG) average_weight = self.order_average_weight if for_customer: average_weight *= qty if order_unit == PRODUCT_ORDER_UNIT_PC_KG and unit_price_amount is not None: unit_price_amount *= self.order_average_weight if average_weight < 1: average_weight_unit = _('gr') average_weight *= 1000 else: average_weight_unit = _('kg') decimal = 3 if average_weight == int(average_weight): decimal = 0 elif average_weight * 10 == int(average_weight * 10): decimal = 1 elif average_weight * 100 == int(average_weight * 100): decimal = 2 tilde = EMPTY_STRING if order_unit == PRODUCT_ORDER_UNIT_PC_KG: tilde = '~' if for_customer: if qty == DECIMAL_ZERO: unit = EMPTY_STRING else: if self.order_average_weight == 1 and order_unit == PRODUCT_ORDER_UNIT_PC_PRICE_KG: unit = "%s%s %s" % ( tilde, number_format(average_weight, decimal), average_weight_unit) else: unit = "%s%s%s" % ( tilde, number_format(average_weight, decimal), average_weight_unit) else: if qty == DECIMAL_ZERO: unit = EMPTY_STRING else: unit = "%s%s%s" % (tilde, number_format(average_weight, decimal), average_weight_unit) elif order_unit == PRODUCT_ORDER_UNIT_PC_PRICE_LT: display_qty = self.order_average_weight != 1 average_weight = self.order_average_weight if for_customer: average_weight *= qty if average_weight < 1: average_weight_unit = _('cl') average_weight *= 100 else: average_weight_unit = _('l') decimal = 3 if average_weight == int(average_weight): decimal = 0 elif average_weight * 10 == int(average_weight * 10): decimal = 1 elif average_weight * 100 == int(average_weight * 100): decimal = 2 if for_customer: if qty == DECIMAL_ZERO: unit = EMPTY_STRING else: if display_qty: unit = "%s%s" % (number_format( average_weight, decimal), average_weight_unit) else: unit = "%s %s" % (number_format( average_weight, decimal), average_weight_unit) else: if qty == DECIMAL_ZERO: unit = EMPTY_STRING else: unit = "%s%s" % (number_format( average_weight, decimal), average_weight_unit) elif order_unit == PRODUCT_ORDER_UNIT_PC_PRICE_PC: display_qty = self.order_average_weight != 1 average_weight = self.order_average_weight if for_customer: average_weight *= qty if qty == DECIMAL_ZERO: unit = EMPTY_STRING else: if average_weight < 2: pc_pcs = _('pc') else: pc_pcs = _('pcs') if display_qty: unit = "%s%s" % (number_format(average_weight, 0), pc_pcs) else: unit = "%s %s" % (number_format(average_weight, 0), pc_pcs) else: if average_weight == DECIMAL_ZERO: unit = EMPTY_STRING elif average_weight < 2: unit = '%s %s' % (number_format(average_weight, 0), _('pc')) else: unit = '%s %s' % (number_format(average_weight, 0), _('pcs')) else: if for_order_select: if qty == DECIMAL_ZERO: unit = EMPTY_STRING elif qty < 2: unit = "%s" % (_('unit')) else: unit = "%s" % (_('units')) else: unit = EMPTY_STRING if unit_price_amount is not None: price_display = " = %s" % RepanierMoney(unit_price_amount * qty) else: price_display = EMPTY_STRING if magnitude is not None: qty *= magnitude decimal = 3 if qty == int(qty): decimal = 0 elif qty * 10 == int(qty * 10): decimal = 1 elif qty * 100 == int(qty * 100): decimal = 2 if for_customer or for_order_select: if unit: if display_qty: qty_display = "%s (%s)" % (number_format(qty, decimal), unit) else: qty_display = "%s" % unit else: qty_display = "%s" % number_format(qty, decimal) else: if unit: qty_display = "(%s)" % unit else: qty_display = EMPTY_STRING if without_price_display: return qty_display else: display = "%s%s" % (qty_display, price_display) return display def get_qty_display(self, is_quantity_invoiced=False): if self.is_box: # To avoid unicode error in email_offer.send_open_order qty_display = BOX_UNICODE else: if is_quantity_invoiced and self.order_unit == PRODUCT_ORDER_UNIT_PC_KG: qty_display = self.get_display( qty=1, order_unit=PRODUCT_ORDER_UNIT_KG, for_customer=False, without_price_display=True) else: qty_display = self.get_display(qty=1, order_unit=self.order_unit, for_customer=False, without_price_display=True) return qty_display def get_qty_and_price_display(self, is_quantity_invoiced=False, customer_price=True): qty_display = self.get_qty_display(is_quantity_invoiced) unit_price = self.get_unit_price(customer_price=customer_price) if len(qty_display) > 0: if self.unit_deposit.amount > DECIMAL_ZERO: return '%s; %s + ♻ %s' % (qty_display, unit_price, self.unit_deposit) else: return '%s; %s' % (qty_display, unit_price) else: if self.unit_deposit.amount > DECIMAL_ZERO: return '%s + ♻ %s' % (unit_price, self.unit_deposit) else: return '%s' % unit_price def get_customer_alert_order_quantity(self): if self.limit_order_quantity_to_stock: return "%s" % _("Current stock") return self.customer_alert_order_quantity get_customer_alert_order_quantity.short_description = ( _("customer_alert_order_quantity")) get_customer_alert_order_quantity.allow_tags = False def get_order_name(self): qty_display = self.get_qty_display() if qty_display: return '%s %s' % (self.long_name, qty_display) return '%s' % self.long_name def get_long_name_with_producer_price(self): return self.get_long_name(customer_price=False) get_long_name_with_producer_price.short_description = (_("Long name")) get_long_name_with_producer_price.allow_tags = False get_long_name_with_producer_price.admin_order_field = 'translations__long_name' def get_long_name(self, is_quantity_invoiced=False, customer_price=True, with_box_unicode=True): qty_and_price_display = self.get_qty_and_price_display( is_quantity_invoiced, customer_price) if qty_and_price_display: result = '%s %s' % (self.long_name, qty_and_price_display) else: result = '%s' % self.long_name return result get_long_name.short_description = (_("Long name")) get_long_name.allow_tags = False get_long_name.admin_order_field = 'translations__long_name' def get_long_name_with_producer(self, is_quantity_invoiced=False): if self.id is not None: return '%s, %s' % (self.producer.short_profile_name, self.get_long_name( is_quantity_invoiced=is_quantity_invoiced)) else: # Nedeed for django import export since django_import_export-0.4.5 return 'N/A' get_long_name_with_producer.short_description = (_("Long name")) get_long_name_with_producer.allow_tags = False get_long_name_with_producer.admin_order_field = 'translations__long_name' def __str__(self): return self.display() class Meta: abstract = True
class Configuration(TranslatableModel): group_name = models.CharField( _("Name of the group"), max_length=50, default=settings.REPANIER_SETTINGS_GROUP_NAME, ) login_attempt_counter = models.DecimalField( _("Login attempt counter"), default=DECIMAL_ZERO, max_digits=2, decimal_places=0 ) password_reset_on = models.DateTimeField( _("Password reset on"), null=True, blank=True, default=None ) name = models.CharField( max_length=3, choices=LUT_PERMANENCE_NAME, default=PERMANENCE_NAME_PERMANENCE, verbose_name=_("Offers name"), ) currency = models.CharField( max_length=3, choices=LUT_CURRENCY, default=CURRENCY_EUR, verbose_name=_("Currency"), ) max_week_wo_participation = models.DecimalField( _("Alert the customer after this number of weeks without participation"), help_text=_("0 mean : never display a pop up."), default=DECIMAL_ZERO, max_digits=2, decimal_places=0, validators=[MinValueValidator(0)], ) send_abstract_order_mail_to_customer = models.BooleanField( _("Send abstract order mail to customers"), default=False ) send_order_mail_to_board = models.BooleanField( _("Send an order distribution email to members registered for a task"), default=True, ) send_invoice_mail_to_customer = models.BooleanField( _("Send invoice mail to customers"), default=True ) send_invoice_mail_to_producer = models.BooleanField( _("Send invoice mail to producers"), default=False ) invoice = models.BooleanField(_("Enable accounting module"), default=True) display_anonymous_order_form = models.BooleanField( _("Allow the anonymous visitor to see the customer order screen"), default=True ) display_who_is_who = models.BooleanField( _('Display the "who\'s who"'), default=True ) xlsx_portrait = models.BooleanField( _("Always generate XLSX files in portrait mode"), default=False ) bank_account = models.CharField( _("Bank account"), max_length=100, blank=True, default=EMPTY_STRING ) vat_id = models.CharField( _("VAT id"), max_length=20, blank=True, default=EMPTY_STRING ) page_break_on_customer_check = models.BooleanField( _("Page break on customer check"), default=False ) membership_fee = ModelMoneyField( _("Membership fee"), default=DECIMAL_ZERO, max_digits=8, decimal_places=2 ) membership_fee_duration = models.DecimalField( _("Membership fee duration"), help_text=_("Number of month(s). 0 mean : no membership fee."), default=DECIMAL_ZERO, max_digits=3, decimal_places=0, validators=[MinValueValidator(0)], ) home_site = models.URLField(_("Home site"), blank=True, default="/") permanence_of_last_cancelled_invoice = models.ForeignKey( "Permanence", on_delete=models.PROTECT, blank=True, null=True ) translations = TranslatedFields( group_label=models.CharField( _("Label to mention on the invoices of the group"), max_length=100, default=EMPTY_STRING, blank=True, ), how_to_register=HTMLField( _("How to register"), help_text=EMPTY_STRING, configuration="CKEDITOR_SETTINGS_MODEL2", default=EMPTY_STRING, blank=True, ), offer_customer_mail=HTMLField( _( "Contents of the order opening email sent to consumers authorized to order" ), help_text=EMPTY_STRING, configuration="CKEDITOR_SETTINGS_MODEL2", default=EMPTY_STRING, blank=True, ), order_customer_mail=HTMLField( _( "Content of the order confirmation email sent to the consumers concerned" ), help_text=EMPTY_STRING, configuration="CKEDITOR_SETTINGS_MODEL2", default=EMPTY_STRING, blank=True, ), cancel_order_customer_mail=HTMLField( _( "Content of the email in case of cancellation of the order sent to the consumers concerned" ), help_text=EMPTY_STRING, configuration="CKEDITOR_SETTINGS_MODEL2", default=EMPTY_STRING, blank=True, ), order_staff_mail=HTMLField( _( "Content of the order distribution email sent to the members enrolled to a task" ), help_text=EMPTY_STRING, configuration="CKEDITOR_SETTINGS_MODEL2", default=EMPTY_STRING, blank=True, ), order_producer_mail=HTMLField( _( "Content of the order confirmation email sent to the producers concerned" ), help_text=EMPTY_STRING, configuration="CKEDITOR_SETTINGS_MODEL2", default=EMPTY_STRING, blank=True, ), invoice_customer_mail=HTMLField( _( "Content of the invoice confirmation email sent to the customers concerned" ), help_text=EMPTY_STRING, configuration="CKEDITOR_SETTINGS_MODEL2", default=EMPTY_STRING, blank=True, ), invoice_producer_mail=HTMLField( _( "Content of the payment confirmation email sent to the producers concerned" ), help_text=EMPTY_STRING, configuration="CKEDITOR_SETTINGS_MODEL2", default=EMPTY_STRING, blank=True, ), ) def clean(self): try: template = Template(self.offer_customer_mail) except Exception as error_str: raise ValidationError( mark_safe("{} : {}".format(self.offer_customer_mail, error_str)) ) try: template = Template(self.order_customer_mail) except Exception as error_str: raise ValidationError( mark_safe("{} : {}".format(self.order_customer_mail, error_str)) ) try: template = Template(self.order_staff_mail) except Exception as error_str: raise ValidationError( mark_safe("{} : {}".format(self.order_staff_mail, error_str)) ) try: template = Template(self.order_producer_mail) except Exception as error_str: raise ValidationError( mark_safe("{} : {}".format(self.order_producer_mail, error_str)) ) if settings.REPANIER_SETTINGS_MANAGE_ACCOUNTING: try: template = Template(self.invoice_customer_mail) except Exception as error_str: raise ValidationError( mark_safe("{} : {}".format(self.invoice_customer_mail, error_str)) ) try: template = Template(self.invoice_producer_mail) except Exception as error_str: raise ValidationError( mark_safe("{} : {}".format(self.invoice_producer_mail, error_str)) ) @classmethod def init_repanier(cls): from repanier.const import DECIMAL_ONE, PERMANENCE_NAME_PERMANENCE, CURRENCY_EUR from repanier.models.producer import Producer from repanier.models.bankaccount import BankAccount from repanier.models.staff import Staff from repanier.models.customer import Customer from repanier.models.lut import LUT_DepartmentForCustomer logger.debug("######## start of init_repanier") # Create the configuration record managed via the admin UI config = Configuration.objects.filter(id=DECIMAL_ONE).first() if config is not None: return config site = Site.objects.get_current() if site is not None: site.name = settings.REPANIER_SETTINGS_GROUP_NAME site.domain = settings.ALLOWED_HOSTS[0] site.save() config = Configuration.objects.create( group_name=settings.REPANIER_SETTINGS_GROUP_NAME, name=PERMANENCE_NAME_PERMANENCE, bank_account="BE99 9999 9999 9999", currency=CURRENCY_EUR, ) config.init_email() config.save() # Create firsts users Producer.get_or_create_group() customer_buyinggroup = Customer.get_or_create_group() very_first_customer = Customer.get_or_create_the_very_first_customer() BankAccount.open_account( customer_buyinggroup=customer_buyinggroup, very_first_customer=very_first_customer, ) very_first_customer = Customer.get_or_create_the_very_first_customer() coordinator = Staff.get_or_create_any_coordinator() Staff.get_or_create_order_responsible() Staff.get_or_create_invoice_responsible() # Create and publish first web page if not coordinator.is_webmaster: # This should not be the case... return from cms.models import StaticPlaceholder from cms.constants import X_FRAME_OPTIONS_DENY from cms import api page = api.create_page( title=_("Home"), soft_root=False, template=settings.CMS_TEMPLATE_HOME, language=settings.LANGUAGE_CODE, published=True, parent=None, xframe_options=X_FRAME_OPTIONS_DENY, in_navigation=True, ) try: # New in CMS 3.5 page.set_as_homepage() except: pass placeholder = page.placeholders.get(slot="home-hero") api.add_plugin( placeholder=placeholder, plugin_type="TextPlugin", language=settings.LANGUAGE_CODE, body=settings.CMS_TEMPLATE_HOME_HERO, ) static_placeholder = StaticPlaceholder( code="footer", # site_id=1 ) static_placeholder.save() api.add_plugin( placeholder=static_placeholder.draft, plugin_type="TextPlugin", language=settings.LANGUAGE_CODE, body="hello world footer", ) static_placeholder.publish( request=None, language=settings.LANGUAGE_CODE, force=True ) api.publish_page( page=page, user=coordinator.customer_responsible.user, language=settings.LANGUAGE_CODE, ) if LUT_DepartmentForCustomer.objects.count() == 0: # Generate a template of LUT_DepartmentForCustomer parent = LUT_DepartmentForCustomer.objects.create(short_name=_("Vegetable")) LUT_DepartmentForCustomer.objects.create( short_name=_("Basket of vegetables"), parent=parent ) LUT_DepartmentForCustomer.objects.create( short_name=_("Salad"), parent=parent ) LUT_DepartmentForCustomer.objects.create( short_name=_("Tomato"), parent=parent ) LUT_DepartmentForCustomer.objects.create( short_name=_("Potato"), parent=parent ) LUT_DepartmentForCustomer.objects.create( short_name=_("Green"), parent=parent ) LUT_DepartmentForCustomer.objects.create( short_name=_("Cabbage"), parent=parent ) parent = LUT_DepartmentForCustomer.objects.create(short_name=_("Fruit")) LUT_DepartmentForCustomer.objects.create( short_name=_("Basket of fruits"), parent=parent ) LUT_DepartmentForCustomer.objects.create( short_name=_("Apple"), parent=parent ) LUT_DepartmentForCustomer.objects.create( short_name=_("Pear"), parent=parent ) LUT_DepartmentForCustomer.objects.create( short_name=_("Plum"), parent=parent ) parent = LUT_DepartmentForCustomer.objects.create(short_name=_("Bakery")) LUT_DepartmentForCustomer.objects.create( short_name=_("Flour"), parent=parent ) LUT_DepartmentForCustomer.objects.create( short_name=_("Bread"), parent=parent ) LUT_DepartmentForCustomer.objects.create( short_name=_("Pastry"), parent=parent ) parent = LUT_DepartmentForCustomer.objects.create(short_name=_("Butchery")) LUT_DepartmentForCustomer.objects.create( short_name=_("Delicatessen"), parent=parent ) LUT_DepartmentForCustomer.objects.create( short_name=_("Chicken"), parent=parent ) LUT_DepartmentForCustomer.objects.create( short_name=_("Pork"), parent=parent ) LUT_DepartmentForCustomer.objects.create( short_name=_("Beef"), parent=parent ) LUT_DepartmentForCustomer.objects.create( short_name=_("Beef and pork"), parent=parent ) LUT_DepartmentForCustomer.objects.create( short_name=_("Veal"), parent=parent ) LUT_DepartmentForCustomer.objects.create( short_name=_("Lamb"), parent=parent ) parent = LUT_DepartmentForCustomer.objects.create(short_name=_("Grocery")) LUT_DepartmentForCustomer.objects.create( short_name=_("Takeaway"), parent=parent ) LUT_DepartmentForCustomer.objects.create( short_name=_("Pasta"), parent=parent ) LUT_DepartmentForCustomer.objects.create( short_name=_("Chocolate"), parent=parent ) LUT_DepartmentForCustomer.objects.create(short_name=_("Oil"), parent=parent) LUT_DepartmentForCustomer.objects.create(short_name=_("Egg"), parent=parent) LUT_DepartmentForCustomer.objects.create(short_name=_("Jam"), parent=parent) LUT_DepartmentForCustomer.objects.create( short_name=_("Cookie"), parent=parent ) parent = LUT_DepartmentForCustomer.objects.create(short_name=_("Creamery")) LUT_DepartmentForCustomer.objects.create( short_name=_("Dairy"), parent=parent ) LUT_DepartmentForCustomer.objects.create( short_name=_("Cow cheese"), parent=parent ) LUT_DepartmentForCustomer.objects.create( short_name=_("Goat cheese"), parent=parent ) LUT_DepartmentForCustomer.objects.create( short_name=_("Sheep cheese"), parent=parent ) LUT_DepartmentForCustomer.objects.create( short_name=_("Mixed cheese"), parent=parent ) parent = LUT_DepartmentForCustomer.objects.create(short_name=_("Icecream")) LUT_DepartmentForCustomer.objects.create( short_name=_("Cup of icecream"), parent=parent ) LUT_DepartmentForCustomer.objects.create( short_name=_("Icecream per liter"), parent=parent ) LUT_DepartmentForCustomer.objects.create( short_name=_("Icecream in frisco"), parent=parent ) LUT_DepartmentForCustomer.objects.create( short_name=_("Icecream cake"), parent=parent ) parent = LUT_DepartmentForCustomer.objects.create(short_name=_("Sorbet")) LUT_DepartmentForCustomer.objects.create( short_name=_("Cup of sorbet"), parent=parent ) LUT_DepartmentForCustomer.objects.create( short_name=_("Sorbet per liter"), parent=parent ) parent = LUT_DepartmentForCustomer.objects.create(short_name=_("Drink")) LUT_DepartmentForCustomer.objects.create( short_name=_("Juice"), parent=parent ) LUT_DepartmentForCustomer.objects.create( short_name=_("Coffee"), parent=parent ) LUT_DepartmentForCustomer.objects.create(short_name=_("Tea"), parent=parent) LUT_DepartmentForCustomer.objects.create( short_name=_("Herbal tea"), parent=parent ) LUT_DepartmentForCustomer.objects.create( short_name=_("Wine"), parent=parent ) LUT_DepartmentForCustomer.objects.create( short_name=_("Aperitif"), parent=parent ) LUT_DepartmentForCustomer.objects.create( short_name=_("Liqueurs"), parent=parent ) parent = LUT_DepartmentForCustomer.objects.create(short_name=_("Hygiene")) parent = LUT_DepartmentForCustomer.objects.create(short_name=_("Deposit")) parent = LUT_DepartmentForCustomer.objects.create( short_name=_("Subscription") ) logger.debug("######## end of init_repanier") return config def init_email(self): for language in settings.PARLER_LANGUAGES[settings.SITE_ID]: language_code = language["code"] self.set_current_language(language_code) try: self.offer_customer_mail = """ Bonjour,<br /> <br /> Les commandes de la {{ permanence_link }} sont maintenant ouvertes auprès de : {{ offer_producer }}.<br /> {% if offer_description %}<br />{{ offer_description }}<br /> {% endif %} {% if offer_recent_detail %}<br /> Nouveauté(s) :<br /> {{ offer_recent_detail }}{% endif %}<br /> <br /> {{ signature }} """ self.order_customer_mail = """ Bonjour {{ long_basket_name }},<br> <br> En pièce jointe vous trouverez le montant de votre panier {{ short_basket_name }} de la {{ permanence_link }}.<br> <br> {{ last_balance }}<br> {{ order_amount }}<br> {% if on_hold_movement %}{{ on_hold_movement }}<br> {% endif %} {% if payment_needed %}{{ payment_needed }}<br> {% endif %}<br> <br> {{ signature }} """ self.cancel_order_customer_mail = """ Bonjour {{ long_basket_name }},<br> <br> La commande ci-jointe de votre panier {{ short_basket_name }} de la {{ permanence_link }} <b>a été annulée</b> car vous ne l'avez pas confirmée.<br> <br> {{ signature }} """ self.order_staff_mail = """ Cher/Chère membre de l'équipe de préparation,<br> <br> En pièce jointe vous trouverez la liste de préparation pour la {{ permanence_link }}.<br> <br> L'équipe de préparation est composée de :<br> {{ board_composition_and_description }}<br> <br> {{ signature }} """ self.order_producer_mail = """ Cher/Chère {{ name }},<br> <br> {% if order_empty %}Le groupe ne vous a rien acheté pour la {{ permanence_link }}.{% else %}En pièce jointe, vous trouverez la commande du groupe pour la {{ permanence }}.{% if duplicate %}<br> <strong>ATTENTION </strong>: La commande est présente en deux exemplaires. Le premier exemplaire est classé par produit et le duplicata est classé par panier.{% else %}{% endif %}{% endif %}<br> <br> {{ signature }} """ self.invoice_customer_mail = """ Bonjour {{ name }},<br> <br> En cliquant sur ce lien vous trouverez votre facture pour la {{ permanence_link }}.{% if invoice_description %}<br> <br> {{ invoice_description }}{% endif %} <br> {{ order_amount }}<br> {{ last_balance_link }}<br> {% if payment_needed %}{{ payment_needed }}<br> {% endif %}<br> <br> {{ signature }} """ self.invoice_producer_mail = """ Cher/Chère {{ profile_name }},<br> <br> En cliquant sur ce lien vous trouverez le détail de notre paiement pour la {{ permanence_link }}.<br> <br> {{ signature }} """ self.save_translations() except TranslationDoesNotExist: pass def __str__(self): return self.group_name class Meta: verbose_name = _("Configuration") verbose_name_plural = _("Configurations")
class LUT_DeliveryPoint(MPTTModel, TranslatableModel): parent = TreeForeignKey("self", null=True, blank=True, related_name="children", on_delete=models.CASCADE) translations = TranslatedFields( short_name=models.CharField(_("Short name"), max_length=50, default=EMPTY_STRING), description=HTMLField( _("Description"), configuration="CKEDITOR_SETTINGS_MODEL2", blank=True, default=EMPTY_STRING, ), ) is_active = models.BooleanField(_("Active"), default=True) # A delivery point may have a customer who is responsible to pay # for all the customers who have selected this delivery point # Such delivery point represent a closed group of customers. customer_responsible = models.ForeignKey( "Customer", verbose_name=_("Customer responsible"), help_text= _("Invoices are sent to this customer who is responsible for collecting the payments." ), blank=True, null=True, default=None, on_delete=models.CASCADE, ) # Does the customer responsible of this delivery point be informed of # each individual order for this delivery point ? inform_customer_responsible = models.BooleanField( _("Inform the group of orders placed by its members"), default=False) transport = ModelMoneyField( _("Delivery point shipping cost"), default=DECIMAL_ZERO, blank=True, max_digits=5, decimal_places=2, validators=[MinValueValidator(0)], ) min_transport = ModelMoneyField( _("Minimum order amount for free shipping cost"), # help_text=_("This is the minimum order amount to avoid shipping cost."), default=DECIMAL_ZERO, blank=True, max_digits=5, decimal_places=2, validators=[MinValueValidator(0)], ) objects = LUT_DeliveryPointManager() def __str__(self): if self.customer_responsible is not None: return "[{}]".format(self.customer_responsible.short_basket_name) else: return self.safe_translation_getter("short_name", any_language=True, default=EMPTY_STRING) class Meta: verbose_name = _("Delivery point") verbose_name_plural = _("Deliveries points")
class Customer(models.Model): user = models.OneToOneField(settings.AUTH_USER_MODEL, db_index=True, on_delete=models.CASCADE) login_attempt_counter = models.DecimalField(_("Login attempt counter"), default=DECIMAL_ZERO, max_digits=2, decimal_places=0) short_basket_name = models.CharField( _("Short name"), max_length=25, blank=False, default=EMPTY_STRING, db_index=True, unique=True, ) long_basket_name = models.CharField(_("Long name"), max_length=100, blank=True, default=EMPTY_STRING) email2 = models.EmailField(_("Secondary email"), null=True, blank=True, default=EMPTY_STRING) language = models.CharField( max_length=5, choices=settings.LANGUAGES, default=settings.LANGUAGE_CODE, verbose_name=_("Language"), ) picture = RepanierPictureField( verbose_name=_("Picture"), null=True, blank=True, upload_to="customer", size=SIZE_S, ) phone1 = models.CharField(_("Phone1"), max_length=25, blank=True, default=EMPTY_STRING) phone2 = models.CharField(_("Phone2"), max_length=25, blank=True, default=EMPTY_STRING) bank_account1 = models.CharField(_("Main bank account"), max_length=100, blank=True, default=EMPTY_STRING) bank_account2 = models.CharField(_("Secondary bank account"), max_length=100, blank=True, default=EMPTY_STRING) vat_id = models.CharField(_("VAT id"), max_length=20, blank=True, default=EMPTY_STRING) address = models.TextField(_("Address"), blank=True, default=EMPTY_STRING) city = models.CharField(_("City"), max_length=50, blank=True, default=EMPTY_STRING) about_me = models.TextField(_("About me"), blank=True, default=EMPTY_STRING) memo = models.TextField(_("Memo"), blank=True, default=EMPTY_STRING) membership_fee_valid_until = models.DateField( _("Membership fee valid until"), default=datetime.date.today) # If this customer is member of a closed group, the customer.price_list_multiplier is not used # Invoices are sent to the consumer responsible of the group who is # also responsible for collecting the payments. # The LUT_DeliveryPoint.price_list_multiplier will be used when invoicing the consumer responsible # At this stage, the link between the customer invoice and this customer responsible is made with # CustomerInvoice.customer_charged price_list_multiplier = models.DecimalField( _("Coefficient applied to the producer tariff to calculate the consumer tariff" ), help_text= _("This multiplier is applied to each product ordered by this customer." ), default=DECIMAL_ONE, max_digits=5, decimal_places=4, blank=True, validators=[MinValueValidator(0)], ) password_reset_on = models.DateTimeField(_("Password reset on"), null=True, blank=True, default=None) date_balance = models.DateField(_("Date balance"), default=datetime.date.today) balance = ModelMoneyField(_("Balance"), max_digits=8, decimal_places=2, default=DECIMAL_ZERO) # The initial balance is needed to compute the invoice control list initial_balance = ModelMoneyField(_("Initial balance"), max_digits=8, decimal_places=2, default=DECIMAL_ZERO) represent_this_buyinggroup = models.BooleanField( _("Represent_this_buyinggroup"), default=False) delivery_point = models.ForeignKey( "LUT_DeliveryPoint", verbose_name=_("Delivery point"), blank=True, null=True, default=None, on_delete=models.CASCADE, ) is_active = models.BooleanField(_("Active"), default=True) as_staff = models.ForeignKey("Staff", blank=True, null=True, default=None, on_delete=models.CASCADE) # This indicate that the user record data have been replaced with anonymous data in application of GDPR is_anonymized = models.BooleanField(default=False) is_group = models.BooleanField(_("Group"), default=False) may_order = models.BooleanField(_("May order"), default=True) zero_waste = models.BooleanField(_("Zero waste"), default=False) valid_email = models.NullBooleanField(_("Valid email"), default=None) subscribe_to_email = models.BooleanField( _("Agree to receive mails from this site"), default=True) preparation_order = models.IntegerField(null=True, blank=True, default=0) @classmethod def get_or_create_group(cls): customer_buyinggroup = (Customer.objects.filter( represent_this_buyinggroup=True).order_by("?").first()) if customer_buyinggroup is None: long_name = settings.REPANIER_SETTINGS_GROUP_NAME short_name = long_name[:25] user = User.objects.filter( username=short_name).order_by("?").first() if user is None: user = User.objects.create_user( username=short_name, email=settings.DEFAULT_FROM_EMAIL, password=uuid.uuid1().hex, first_name=EMPTY_STRING, last_name=long_name, ) customer_buyinggroup = Customer.objects.create( user=user, short_basket_name=short_name, long_basket_name=long_name, phone1=settings.REPANIER_SETTINGS_COORDINATOR_PHONE, represent_this_buyinggroup=True, ) return customer_buyinggroup @classmethod def get_or_create_the_very_first_customer(cls): very_first_customer = (Customer.objects.filter( represent_this_buyinggroup=False, is_active=True).order_by("id").first()) if very_first_customer is None: long_name = settings.REPANIER_SETTINGS_COORDINATOR_NAME # short_name is the first word of long_name, limited to max. 25 characters short_name = long_name.split(None, 1)[0][:25] user = User.objects.filter( username=short_name).order_by("?").first() if user is None: user = User.objects.create_user( username=short_name, email=settings.REPANIER_SETTINGS_COORDINATOR_EMAIL, password=uuid.uuid1().hex, first_name=EMPTY_STRING, last_name=long_name, ) very_first_customer = Customer.objects.create( user=user, short_basket_name=short_name, long_basket_name=long_name, phone1=settings.REPANIER_SETTINGS_COORDINATOR_PHONE, represent_this_buyinggroup=False, ) return very_first_customer @classmethod def get_customer_from_valid_email(cls, email_address): # try to find a customer based on user__email or customer__email2 customer = (Customer.objects.filter( Q(user__email=email_address) | Q(email2=email_address)).exclude( valid_email=False).order_by("?").first()) return customer def get_admin_date_balance(self): return timezone.now().date().strftime(settings.DJANGO_SETTINGS_DATE) get_admin_date_balance.short_description = _("Date balance") def get_admin_date_joined(self): # New customer have no user during import of customers in admin.customer.CustomerResource try: return self.user.date_joined.strftime( settings.DJANGO_SETTINGS_DATE) except User.DoesNotExist: # RelatedObjectDoesNotExist return EMPTY_STRING get_admin_date_joined.short_description = _("Date joined") def get_admin_balance(self): return (self.balance + self.get_bank_not_invoiced() - self.get_order_not_invoiced()) get_admin_balance.short_description = _("Balance") def get_phone1(self, prefix=EMPTY_STRING, postfix=EMPTY_STRING): # return ", phone1" if prefix = ", " # return " (phone1)" if prefix = " (" and postfix = ")" if not self.phone1: return EMPTY_STRING return "{}{}{}".format(prefix, self.phone1, postfix) def get_phone2(self): return self.phone2 or EMPTY_STRING def get_phones(self, sep=", "): return (sep.join([self.phone1, self.phone2, EMPTY_STRING]) if self.phone2 else sep.join([self.phone1, EMPTY_STRING])) def get_email1(self, prefix=EMPTY_STRING): if not self.user.email: return EMPTY_STRING return "{}{}".format(prefix, self.user.email) def get_emails(self, sep="; "): return (sep.join([self.user.email, self.email2, EMPTY_STRING]) if self.email2 else sep.join([self.user.email, EMPTY_STRING])) def get_order_not_invoiced(self): if settings.REPANIER_SETTINGS_MANAGE_ACCOUNTING: result_set = (CustomerInvoice.objects.filter( customer_id=self.id, status__gte=PERMANENCE_OPENED, status__lte=PERMANENCE_SEND, customer_charged_id=self.id, ).order_by("?").aggregate( total_price=Sum( "total_price_with_tax", output_field=DecimalField(max_digits=8, decimal_places=2, default=DECIMAL_ZERO), ), delta_price=Sum( "delta_price_with_tax", output_field=DecimalField(max_digits=8, decimal_places=2, default=DECIMAL_ZERO), ), delta_transport=Sum( "delta_transport", output_field=DecimalField(max_digits=5, decimal_places=2, default=DECIMAL_ZERO), ), )) total_price = (result_set["total_price"] if result_set["total_price"] is not None else DECIMAL_ZERO) delta_price = (result_set["delta_price"] if result_set["delta_price"] is not None else DECIMAL_ZERO) delta_transport = (result_set["delta_transport"] if result_set["delta_transport"] is not None else DECIMAL_ZERO) order_not_invoiced = RepanierMoney(total_price + delta_price + delta_transport) else: order_not_invoiced = REPANIER_MONEY_ZERO return order_not_invoiced def get_bank_not_invoiced(self): if settings.REPANIER_SETTINGS_MANAGE_ACCOUNTING: result_set = (BankAccount.objects.filter( customer_id=self.id, customer_invoice__isnull=True).order_by("?").aggregate( bank_in=Sum( "bank_amount_in", output_field=DecimalField(max_digits=8, decimal_places=2, default=DECIMAL_ZERO), ), bank_out=Sum( "bank_amount_out", output_field=DecimalField(max_digits=8, decimal_places=2, default=DECIMAL_ZERO), ), )) bank_in = (result_set["bank_in"] if result_set["bank_in"] is not None else DECIMAL_ZERO) bank_out = (result_set["bank_out"] if result_set["bank_out"] is not None else DECIMAL_ZERO) bank_not_invoiced = bank_in - bank_out else: bank_not_invoiced = DECIMAL_ZERO return RepanierMoney(bank_not_invoiced) def get_balance(self): last_customer_invoice = CustomerInvoice.objects.filter( customer_id=self.id, invoice_sort_order__isnull=False).order_by("?") balance = self.get_admin_balance() if last_customer_invoice.exists(): if balance.amount >= 30: return format_html( '<a href="{}?customer={}" class="btn" target="_blank" ><span style="color:#32CD32">{}</span></a>', reverse("customer_invoice_view", args=(0, )), str(self.id), balance, ) elif balance.amount >= -10: return format_html( '<a href="{}?customer={}" class="btn" target="_blank" ><span style="color:#696969">{}</span></a>', reverse("customer_invoice_view", args=(0, )), str(self.id), balance, ) else: return format_html( '<a href="{}?customer={}" class="btn" target="_blank" ><span style="color:red">{}</span></a>', reverse("customer_invoice_view", args=(0, )), str(self.id), balance, ) else: if balance.amount >= 30: return format_html('<span style="color:#32CD32">{}</span>', balance) elif balance.amount >= -10: return format_html('<span style="color:#696969">{}</span>', balance) else: return format_html('<span style="color:red">{}</span>', balance) get_balance.short_description = _("Balance") get_balance.admin_order_field = "balance" def get_html_on_hold_movement( self, bank_not_invoiced=None, order_not_invoiced=None, total_price_with_tax=REPANIER_MONEY_ZERO, ): if settings.REPANIER_SETTINGS_MANAGE_ACCOUNTING: bank_not_invoiced = (bank_not_invoiced if bank_not_invoiced is not None else self.get_bank_not_invoiced()) order_not_invoiced = (order_not_invoiced if order_not_invoiced is not None else self.get_order_not_invoiced()) other_order_not_invoiced = order_not_invoiced - total_price_with_tax else: bank_not_invoiced = REPANIER_MONEY_ZERO other_order_not_invoiced = REPANIER_MONEY_ZERO if (other_order_not_invoiced.amount != DECIMAL_ZERO or bank_not_invoiced.amount != DECIMAL_ZERO): if other_order_not_invoiced.amount != DECIMAL_ZERO: if bank_not_invoiced.amount == DECIMAL_ZERO: customer_on_hold_movement = _( "This balance does not take account of any unbilled sales %(other_order)s." ) % { "other_order": other_order_not_invoiced } else: customer_on_hold_movement = _( "This balance does not take account of any unrecognized payments %(bank)s and any unbilled order %(other_order)s." ) % { "bank": bank_not_invoiced, "other_order": other_order_not_invoiced, } else: customer_on_hold_movement = _( "This balance does not take account of any unrecognized payments %(bank)s." ) % { "bank": bank_not_invoiced } customer_on_hold_movement = mark_safe(customer_on_hold_movement) else: customer_on_hold_movement = EMPTY_STRING return customer_on_hold_movement def get_last_membership_fee(self): from repanier.models.purchase import Purchase last_membership_fee = Purchase.objects.filter( customer_id=self.id, offer_item__order_unit=PRODUCT_ORDER_UNIT_MEMBERSHIP_FEE, ).order_by("-id") if last_membership_fee.exists(): return last_membership_fee.first().selling_price get_last_membership_fee.short_description = _("Last membership fee") def last_membership_fee_date(self): from repanier.models.purchase import Purchase last_membership_fee = (Purchase.objects.filter( customer_id=self.id, offer_item__order_unit=PRODUCT_ORDER_UNIT_MEMBERSHIP_FEE, ).order_by("-id").prefetch_related("customer_invoice")) if last_membership_fee.exists(): return last_membership_fee.first().customer_invoice.date_balance last_membership_fee_date.short_description = _("Last membership fee date") def get_last_membership_fee_date(self): # Format it for the admin # Don't format it form import/export last_membership_fee_date = self.last_membership_fee_date() if last_membership_fee_date is not None: return last_membership_fee_date.strftime( settings.DJANGO_SETTINGS_DATE) return EMPTY_STRING get_last_membership_fee_date.short_description = _( "Last membership fee date") def get_participation(self): now = timezone.now() return (PermanenceBoard.objects.filter( customer_id=self.id, permanence_date__gte=now - datetime.timedelta(days=ONE_YEAR), permanence_date__lt=now, permanence_role__is_counted_as_participation=True, ).order_by("?").count()) get_participation.short_description = _("Participation") def get_purchase(self): now = timezone.now() # Do not count invoice having only products free of charge return CustomerInvoice.objects.filter( customer_id=self.id, total_price_with_tax__gt=DECIMAL_ZERO, date_balance__gte=now - datetime.timedelta(ONE_YEAR), ).count() get_purchase.short_description = _("Purchase") def my_order_confirmation_email_send_to(self): if self.email2: to_email = (self.user.email, self.email2) else: to_email = (self.user.email, ) sent_to = ", ".join(to_email) if to_email is not None else EMPTY_STRING if settings.REPANIER_SETTINGS_CUSTOMER_MUST_CONFIRM_ORDER: msg_confirmation = _( "Order confirmed. An email containing this order summary has been sent to {}." ).format(sent_to) else: msg_confirmation = _( "An email containing this order summary has been sent to {}." ).format(sent_to) return msg_confirmation def get_html_unsubscribe_mail_footer(self): return mark_safe('<br><br><hr/><br><a href="{}">{}</a>'.format( self._get_unsubscribe_link(), _("Stop receiving mails from {}").format( self._get_unsubscribe_site()), )) def get_html_list_unsubscribe(self): return mark_safe("<{}>".format(self._get_unsubscribe_link())) def _get_unsubscribe_link(self): customer_id, token = self.make_token().split(":", 1) return "https://{}{}".format( self._get_unsubscribe_site(), reverse("unsubscribe_view", kwargs={ "customer_id": customer_id, "token": token }), ) @staticmethod def _get_unsubscribe_site(): return settings.ALLOWED_HOSTS[0] def make_token(self): return TimestampSigner().sign(self.id) def check_token(self, token): try: key = "{}:{}".format(self.id, token) TimestampSigner().unsign(key, max_age=60 * 60 * 48) # Valid for 2 days except (BadSignature, SignatureExpired): return False return True def anonymize(self, also_group=False): if self.represent_this_buyinggroup: if not also_group: return self.short_basket_name = "{}-{}".format(_("GROUP"), self.id).lower() self.long_basket_name = "{} {}".format(_("Group"), self.id) else: self.short_basket_name = "{}-{}".format(_("BASKET"), self.id).lower() self.long_basket_name = "{} {}".format(_("Family"), self.id) self.email2 = EMPTY_STRING self.picture = EMPTY_STRING self.phone1 = EMPTY_STRING self.phone2 = EMPTY_STRING self.bank_account1 = EMPTY_STRING self.bank_account1 = EMPTY_STRING self.vat_id = EMPTY_STRING self.address = EMPTY_STRING self.about_me = EMPTY_STRING self.memo = EMPTY_STRING self.user.username = self.user.email = "{}@repanier.be".format( self.short_basket_name) self.user.first_name = EMPTY_STRING self.user.last_name = self.short_basket_name self.user.set_password(None) self.user.save() self.is_anonymized = True self.valid_email = False self.subscribe_to_email = False self.save() def __str__(self): if self.delivery_point is None: return self.short_basket_name else: return "{} - {}".format(self.delivery_point, self.short_basket_name) class Meta: verbose_name = _("Customer") verbose_name_plural = _("Customers") ordering = ("-represent_this_buyinggroup", "short_basket_name") indexes = [ models.Index( fields=["-represent_this_buyinggroup", "short_basket_name"], name="customer_order_idx", ) ]
class ProducerInvoice(Invoice): producer = models.ForeignKey( "Producer", verbose_name=_("Producer"), # related_name='producer_invoice', on_delete=models.PROTECT, ) delta_stock_with_tax = ModelMoneyField( _("Amount deducted from the stock"), default=DECIMAL_ZERO, max_digits=8, decimal_places=2, ) delta_stock_vat = ModelMoneyField( _("Total VAT deducted from the stock"), default=DECIMAL_ZERO, max_digits=9, decimal_places=4, ) delta_deposit = ModelMoneyField( _("Deposit"), help_text=_("+ Deposit"), default=DECIMAL_ZERO, max_digits=8, decimal_places=2, ) delta_stock_deposit = ModelMoneyField( _("Deposit"), help_text=_("+ Deposit"), default=DECIMAL_ZERO, max_digits=8, decimal_places=2, ) to_be_paid = models.BooleanField( _("To be paid"), choices=LUT_BANK_NOTE, default=False ) calculated_invoiced_balance = ModelMoneyField( _("Amount due to the producer as calculated by Repanier"), max_digits=8, decimal_places=2, default=DECIMAL_ZERO, ) to_be_invoiced_balance = ModelMoneyField( _("Amount claimed by the producer"), max_digits=8, decimal_places=2, default=DECIMAL_ZERO, ) invoice_sort_order = models.IntegerField( _("Invoice sort order"), default=None, blank=True, null=True, db_index=True ) invoice_reference = models.CharField( _("Invoice reference"), max_length=100, blank=True, default=EMPTY_STRING ) def get_negative_previous_balance(self): return -self.previous_balance def get_negative_balance(self): return -self.balance def get_total_price_with_tax(self): return ( self.total_price_with_tax + self.delta_price_with_tax + self.delta_transport + self.delta_stock_with_tax ) def get_total_vat(self): return self.total_vat + self.delta_stock_vat def get_total_deposit(self): return self.total_deposit + self.delta_stock_deposit def get_order_json(self): a_producer = self.producer json_dict = {} if a_producer.minimum_order_value.amount > DECIMAL_ZERO: ratio = ( self.total_price_with_tax.amount / a_producer.minimum_order_value.amount ) if ratio >= DECIMAL_ONE: ratio = 100 else: ratio *= 100 json_dict["#order_procent{}".format(a_producer.id)] = "{}%".format( number_format(ratio, 0) ) return json_dict def __str__(self): return "{}, {}".format(self.producer, self.permanence) class Meta: verbose_name = _("Producer invoice") verbose_name_plural = _("Producers invoices") unique_together = (("permanence", "producer"),)
class BankAccount(models.Model): permanence = models.ForeignKey( 'Permanence', verbose_name=REPANIER_SETTINGS_PERMANENCE_NAME, on_delete=models.PROTECT, blank=True, null=True) producer = models.ForeignKey('Producer', verbose_name=_("producer"), on_delete=models.PROTECT, blank=True, null=True) customer = models.ForeignKey('Customer', verbose_name=_("customer"), on_delete=models.PROTECT, blank=True, null=True) operation_date = models.DateField(_("operation_date"), db_index=True) operation_comment = models.CharField(_("operation_comment"), max_length=100, null=True, blank=True) operation_status = models.CharField(max_length=3, choices=LUT_BANK_TOTAL, default=BANK_NOT_LATEST_TOTAL, verbose_name=_("Bank balance status"), db_index=True) bank_amount_in = ModelMoneyField(_("bank_amount_in"), help_text=_('payment_on_the_account'), max_digits=8, decimal_places=2, default=DECIMAL_ZERO, validators=[MinValueValidator(0)]) bank_amount_out = ModelMoneyField(_("bank_amount_out"), help_text=_('payment_from_the_account'), max_digits=8, decimal_places=2, default=DECIMAL_ZERO, validators=[MinValueValidator(0)]) producer_invoice = models.ForeignKey('ProducerInvoice', verbose_name=_("producer_invoice"), blank=True, null=True, on_delete=models.PROTECT, db_index=True) customer_invoice = models.ForeignKey('CustomerInvoice', verbose_name=_("customer_invoice"), blank=True, null=True, on_delete=models.PROTECT, db_index=True) is_updated_on = models.DateTimeField(_("is_updated_on"), auto_now=True) def get_bank_amount_in(self): if self.operation_status in [BANK_PROFIT, BANK_TAX]: return "<i>%s</i>" % (self.bank_amount_in if self.bank_amount_in.amount != DECIMAL_ZERO else EMPTY_STRING) else: return self.bank_amount_in if self.bank_amount_in.amount != DECIMAL_ZERO else EMPTY_STRING get_bank_amount_in.short_description = (_("bank_amount_in")) get_bank_amount_in.allow_tags = True get_bank_amount_in.admin_order_field = 'bank_amount_in' def get_bank_amount_out(self): if self.operation_status in [BANK_PROFIT, BANK_TAX]: return "<i>%s</i>" % (self.bank_amount_out if self.bank_amount_out.amount != DECIMAL_ZERO else EMPTY_STRING) else: return self.bank_amount_out if self.bank_amount_out.amount != DECIMAL_ZERO else EMPTY_STRING get_bank_amount_out.short_description = (_("bank_amount_out")) get_bank_amount_out.allow_tags = True get_bank_amount_out.admin_order_field = 'bank_amount_out' def get_producer(self): if self.producer is not None: return self.producer.short_profile_name else: if self.customer is None: # This is a total, show it from repanier.apps import REPANIER_SETTINGS_GROUP_NAME if self.operation_status == BANK_LATEST_TOTAL: return "<b>%s</b>" % "=== %s" % REPANIER_SETTINGS_GROUP_NAME else: return "<b>%s</b>" % "--- %s" % REPANIER_SETTINGS_GROUP_NAME return EMPTY_STRING get_producer.short_description = (_("producer")) get_producer.allow_tags = True get_producer.admin_order_field = 'producer' def get_customer(self): if self.customer is not None: return self.customer.short_basket_name else: if self.producer is None: # This is a total, show it from repanier.apps import REPANIER_SETTINGS_BANK_ACCOUNT if self.operation_status == BANK_LATEST_TOTAL: if REPANIER_SETTINGS_BANK_ACCOUNT is not None: return "<b>%s</b>" % REPANIER_SETTINGS_BANK_ACCOUNT else: return "<b>%s</b>" % "==============" else: if REPANIER_SETTINGS_BANK_ACCOUNT is not None: return "<b>%s</b>" % REPANIER_SETTINGS_BANK_ACCOUNT else: return "<b>%s</b>" % "--------------" return EMPTY_STRING get_customer.short_description = (_("customer")) get_customer.allow_tags = True get_customer.admin_order_field = 'customer' class Meta: verbose_name = _("bank account movement") verbose_name_plural = _("bank account movements") ordering = ('-operation_date', '-id') index_together = [ ['operation_date', 'id'], ['customer_invoice', 'operation_date', 'id'], ['producer_invoice', 'operation_date', 'operation_date', 'id'], ['permanence', 'customer', 'producer', 'operation_date', 'id'], ]
class LUT_DeliveryPoint(MPTTModel, TranslatableModel): parent = TreeForeignKey('self', null=True, blank=True, related_name='children') translations = TranslatedFields( short_name=models.CharField(_("Short name"), max_length=50, db_index=True, unique=True, default=EMPTY_STRING), description=HTMLField(_("description"), configuration='CKEDITOR_SETTINGS_MODEL2', blank=True, default=EMPTY_STRING), ) is_active = models.BooleanField(_("is_active"), default=True) customer_responsible = models.ForeignKey( 'Customer', verbose_name=_("customer_responsible"), help_text= _("Invoices are sent to this consumer who is responsible for collecting the payments." ), on_delete=models.PROTECT, blank=True, null=True, default=None) inform_customer_responsible = models.BooleanField( _("inform_customer_responsible"), default=False) # closed_group = models.BooleanField(_("with entitled customer"), default=False) # price_list_multiplier = models.DecimalField( # _("Delivery point price list multiplier"), # help_text=_("This multiplier is applied once for groups with entitled customer."), # default=DECIMAL_ONE, max_digits=5, decimal_places=4, blank=True, # validators=[MinValueValidator(0)]) transport = ModelMoneyField( _("Delivery point transport"), # help_text=_("This amount is added once for groups with entitled customer or at each customer for open groups."), default=DECIMAL_ZERO, blank=True, max_digits=5, decimal_places=2, validators=[MinValueValidator(0)]) min_transport = ModelMoneyField( _("Minium order amount for free shipping cost"), # help_text=_("This is the minimum order amount to avoid shipping cost."), default=DECIMAL_ZERO, blank=True, max_digits=5, decimal_places=2, validators=[MinValueValidator(0)]) objects = LUT_DeliveryPointManager() def __str__(self): if self.customer_responsible: return "[%s] %s" % (_("Group"), self.customer_responsible.short_basket_name) else: return self.safe_translation_getter('short_name', any_language=True, default=EMPTY_STRING) class Meta: verbose_name = _("delivery point") verbose_name_plural = _("deliveries points")
class Purchase(models.Model): permanence = models.ForeignKey( 'Permanence', verbose_name=repanier.apps.REPANIER_SETTINGS_PERMANENCE_NAME, on_delete=models.PROTECT, db_index=True) status = models.CharField(max_length=3, choices=LUT_PERMANENCE_STATUS, default=PERMANENCE_PLANNED, verbose_name=_("Invoice status")) offer_item = models.ForeignKey('OfferItem', verbose_name=_("Offer item"), on_delete=models.PROTECT) producer = models.ForeignKey('Producer', verbose_name=_("Producer"), on_delete=models.PROTECT) customer = models.ForeignKey('Customer', verbose_name=_("Customer"), on_delete=models.PROTECT, db_index=True) customer_producer_invoice = models.ForeignKey('CustomerProducerInvoice', on_delete=models.PROTECT, db_index=True) producer_invoice = models.ForeignKey('ProducerInvoice', verbose_name=_("Producer invoice"), on_delete=models.PROTECT, db_index=True) customer_invoice = models.ForeignKey('CustomerInvoice', verbose_name=_("Customer invoice"), on_delete=models.PROTECT, db_index=True) is_box = models.BooleanField(default=False) is_box_content = models.BooleanField(default=False) quantity_ordered = models.DecimalField(_("Quantity ordered"), max_digits=9, decimal_places=4, default=DECIMAL_ZERO) quantity_contracted = models.DecimalField(_("Quantity contracted"), max_digits=9, decimal_places=4, default=DECIMAL_ZERO) quantity_confirmed = models.DecimalField(_("Quantity confirmed"), max_digits=9, decimal_places=4, default=DECIMAL_ZERO) # 0 if this is not a KG product -> the preparation list for this product will be produced by family # qty if not -> the preparation list for this product will be produced by qty then by family quantity_for_preparation_sort_order = models.DecimalField( _("Quantity for preparation order_by"), max_digits=9, decimal_places=4, default=DECIMAL_ZERO) # If Permanence.status < SEND this is the order quantity # During sending the orders to the producer this become the invoiced quantity # via tools.recalculate_order_amount(..., send_to_producer=True) quantity_invoiced = models.DecimalField(_("Quantity invoiced"), max_digits=9, decimal_places=4, default=DECIMAL_ZERO) purchase_price = ModelMoneyField(_("Producer row price"), max_digits=8, decimal_places=2, default=DECIMAL_ZERO) selling_price = ModelMoneyField(_("Customer row price"), max_digits=8, decimal_places=2, default=DECIMAL_ZERO) producer_vat = ModelMoneyField(_("VAT"), default=DECIMAL_ZERO, max_digits=8, decimal_places=4) customer_vat = ModelMoneyField(_("VAT"), default=DECIMAL_ZERO, max_digits=8, decimal_places=4) deposit = ModelMoneyField( _("Deposit"), help_text=_('Deposit to add to the original unit price'), default=DECIMAL_ZERO, max_digits=8, decimal_places=2, validators=[MinValueValidator(0)]) price_list_multiplier = models.DecimalField( _("Coefficient applied to the producer tariff to calculate the consumer tariff" ), help_text= _("This multiplier is applied to each price automatically imported/pushed." ), default=DECIMAL_ONE, max_digits=5, decimal_places=4, blank=True, validators=[MinValueValidator(0)]) is_resale_price_fixed = models.BooleanField( _("Customer prices are set by the producer"), default=False) comment = models.CharField(_("Comment"), max_length=100, default=EMPTY_STRING, blank=True, null=True) is_updated_on = models.DateTimeField(_("Updated on"), auto_now=True, db_index=True) def get_customer_unit_price(self): offer_item = self.offer_item if self.price_list_multiplier == DECIMAL_ONE: return offer_item.customer_unit_price.amount else: return (offer_item.customer_unit_price.amount * self.price_list_multiplier).quantize(TWO_DECIMALS) get_customer_unit_price.short_description = (_("Customer unit price")) def get_unit_deposit(self): return self.offer_item.unit_deposit.amount def get_customer_unit_vat(self): offer_item = self.offer_item if self.price_list_multiplier == DECIMAL_ONE: return offer_item.customer_vat.amount else: return (offer_item.customer_vat.amount * self.price_list_multiplier).quantize(FOUR_DECIMALS) def get_producer_unit_vat(self): offer_item = self.offer_item if offer_item.manage_production: return self.get_customer_unit_vat() return offer_item.producer_vat.amount def get_selling_price(self): # workaround for a display problem with Money field in the admin list_display return self.selling_price get_selling_price.short_description = (_("Customer row price")) def get_producer_unit_price(self): offer_item = self.offer_item if offer_item.manage_production: return self.get_customer_unit_price() return offer_item.producer_unit_price.amount get_producer_unit_price.short_description = (_("Producer unit price")) def get_html_producer_unit_price(self): if self.offer_item is not None: return mark_safe( _("<b>%(price)s</b>") % {'price': self.get_producer_unit_price()}) return EMPTY_STRING get_html_producer_unit_price.short_description = (_("Producer unit price")) get_html_producer_unit_price.allow_tags = True def get_html_unit_deposit(self): if self.offer_item is not None: return mark_safe( _("<b>%(price)s</b>") % {'price': self.offer_item.unit_deposit}) return EMPTY_STRING get_html_unit_deposit.short_description = (_("Deposit")) def get_permanence_display(self): return self.permanence.get_permanence_display() get_permanence_display.short_description = (_("Permanence")) def get_delivery_display(self): if self.customer_invoice is not None and self.customer_invoice.delivery is not None: return self.customer_invoice.delivery.get_delivery_display(br=True) return EMPTY_STRING get_delivery_display.short_description = (_("Delivery point")) def get_quantity(self): if self.status < PERMANENCE_WAIT_FOR_SEND: return self.quantity_ordered else: return self.quantity_invoiced get_quantity.short_description = (_("Quantity invoiced")) def get_producer_quantity(self): if self.status < PERMANENCE_WAIT_FOR_SEND: return self.quantity_ordered else: offer_item = self.offer_item if offer_item.order_unit == PRODUCT_ORDER_UNIT_PC_KG: if offer_item.order_average_weight != 0: return (self.quantity_invoiced / offer_item.order_average_weight ).quantize(FOUR_DECIMALS) return self.quantity_invoiced def get_long_name(self, customer_price=True): return self.offer_item.get_long_name(customer_price=customer_price) def set_comment(self, comment): if comment: if self.comment: self.comment = cap("{}, {}".format(self.comment, comment), 100) else: self.comment = cap(comment, 100) @transaction.atomic def save(self, *args, **kwargs): if not self.pk: # This code only happens if the objects is not in the database yet. # Otherwise it would have pk customer_invoice = CustomerInvoice.objects.filter( permanence_id=self.permanence_id, customer_id=self.customer_id).only("id").order_by('?').first() if customer_invoice is None: customer_invoice = CustomerInvoice.objects.create( permanence_id=self.permanence_id, customer_id=self.customer_id, customer_charged_id=self.customer_id, status=self.status) customer_invoice.set_delivery(delivery=None) customer_invoice.save() self.customer_invoice = customer_invoice producer_invoice = ProducerInvoice.objects.filter( permanence_id=self.permanence_id, producer_id=self.producer_id).only("id").order_by('?').first() if producer_invoice is None: producer_invoice = ProducerInvoice.objects.create( permanence_id=self.permanence_id, producer_id=self.producer_id, status=self.status) self.producer_invoice = producer_invoice customer_producer_invoice = CustomerProducerInvoice.objects.filter( permanence_id=self.permanence_id, customer_id=self.customer_id, producer_id=self.producer_id).only("id").order_by('?').first() if customer_producer_invoice is None: customer_producer_invoice = CustomerProducerInvoice.objects.create( permanence_id=self.permanence_id, customer_id=self.customer_id, producer_id=self.producer_id, ) self.customer_producer_invoice = customer_producer_invoice super(Purchase, self).save(*args, **kwargs) @transaction.atomic def save_box(self): if self.offer_item.is_box: for content in BoxContent.objects.filter( box_id=self.offer_item.product_id).order_by('?'): content_offer_item = content.product.get_or_create_offer_item( self.permanence) # Select one purchase content_purchase = Purchase.objects.filter( customer_id=self.customer_id, offer_item_id=content_offer_item.id, is_box_content=True).order_by('?').first() if content_purchase is None: content_purchase = Purchase.objects.create( permanence=self.permanence, offer_item=content_offer_item, producer=self.producer, customer=self.customer, quantity_ordered=self.quantity_ordered * content.content_quantity, quantity_invoiced=self.quantity_invoiced * content.content_quantity, is_box_content=True, status=self.status) else: content_purchase.status = self.status content_purchase.quantity_ordered = self.quantity_ordered * content.content_quantity content_purchase.quantity_invoiced = self.quantity_invoiced * content.content_quantity content_purchase.save() content_purchase.permanence.producers.add( content_offer_item.producer) def __str__(self): # Use to not display label (inline_admin_form.original) into the inline form (tabular.html) return EMPTY_STRING class Meta: verbose_name = _("Purchase") verbose_name_plural = _("Purchases") # ordering = ("permanence", "customer", "offer_item", "is_box_content") unique_together = ("customer", "offer_item", "is_box_content") index_together = [["permanence", "customer_invoice"]]
class OfferItem(Item): translations = TranslatedFields( long_name=models.CharField(_("Long name"), max_length=100, default=EMPTY_STRING, blank=True), cache_part_a=HTMLField(default=EMPTY_STRING, blank=True), cache_part_b=HTMLField(default=EMPTY_STRING, blank=True), # Language dependant customer sort order for optimization order_sort_order=models.IntegerField(default=0, db_index=True), # Language dependant preparation sort order for optimization preparation_sort_order=models.IntegerField(default=0, db_index=True), # Language dependant producer sort order for optimization producer_sort_order=models.IntegerField(default=0, db_index=True), ) permanence = models.ForeignKey( "Permanence", verbose_name=REPANIER_SETTINGS_PERMANENCE_NAME, on_delete=models.PROTECT, db_index=True, ) product = models.ForeignKey("Product", verbose_name=_("Product"), on_delete=models.PROTECT) # is a box content is_box_content = models.BooleanField(default=False) producer_price_are_wo_vat = models.BooleanField( _("Producer price are without vat"), default=False) price_list_multiplier = models.DecimalField( _("Coefficient applied to the producer tariff to calculate the consumer tariff" ), help_text= _("This multiplier is applied to each price automatically imported/pushed." ), default=DECIMAL_ZERO, max_digits=5, decimal_places=4, validators=[MinValueValidator(0)], ) is_resale_price_fixed = models.BooleanField( _("The resale price is fixed (boxes, deposit)"), default=False) # Calculated with Purchase : Total producer purchase price vat included total_purchase_with_tax = ModelMoneyField( _("Producer amount invoiced"), default=DECIMAL_ZERO, max_digits=8, decimal_places=2, ) # Calculated with Purchase : Total customer selling price vat included total_selling_with_tax = ModelMoneyField( _("Invoiced to the consumer w TVA"), default=DECIMAL_ZERO, max_digits=8, decimal_places=2, ) # Calculated with Purchase : Quantity invoiced to all customers # If Permanence.status < SEND this is the order quantity # During sending the orders to the producer this become the invoiced quantity # via permanence.recalculate_order_amount(..., send_to_producer=True) quantity_invoiced = models.DecimalField(_("Qty invoiced"), max_digits=9, decimal_places=4, default=DECIMAL_ZERO) use_order_unit_converted = models.BooleanField(default=False) may_order = models.BooleanField(_("May order"), default=True) manage_production = models.BooleanField(default=False) def get_vat_level(self): return self.get_vat_level_display() get_vat_level.short_description = _("VAT rate") get_vat_level.admin_order_field = "vat_level" def get_producer_qty_stock_invoiced(self): # Return quantity to buy to the producer and stock used to deliver the invoiced quantity if self.quantity_invoiced > DECIMAL_ZERO: return self.quantity_invoiced, DECIMAL_ZERO, self.quantity_invoiced return DECIMAL_ZERO, DECIMAL_ZERO, DECIMAL_ZERO def get_html_producer_qty_stock_invoiced(self): invoiced_qty, taken_from_stock, customer_qty = ( self.get_producer_qty_stock_invoiced()) if invoiced_qty == DECIMAL_ZERO: if taken_from_stock == DECIMAL_ZERO: return EMPTY_STRING else: return mark_safe( _("stock %(stock)s") % {"stock": number_format(taken_from_stock, 4)}) else: if taken_from_stock == DECIMAL_ZERO: return mark_safe( _("<b>%(qty)s</b>") % {"qty": number_format(invoiced_qty, 4)}) else: return mark_safe( _("<b>%(qty)s</b> + stock %(stock)s") % { "qty": number_format(invoiced_qty, 4), "stock": number_format(taken_from_stock, 4), }) get_html_producer_qty_stock_invoiced.short_description = _( "Qty invoiced by the producer") get_html_producer_qty_stock_invoiced.admin_order_field = "quantity_invoiced" def get_producer_qty_invoiced(self): invoiced_qty, taken_from_stock, customer_qty = ( self.get_producer_qty_stock_invoiced()) return invoiced_qty def get_producer_unit_price_invoiced(self): if self.producer_unit_price.amount > self.customer_unit_price.amount: return self.customer_unit_price else: return self.producer_unit_price def get_producer_row_price_invoiced(self): if self.producer_unit_price.amount > self.customer_unit_price.amount: return self.total_selling_with_tax else: return self.total_purchase_with_tax def get_html_producer_price_purchased(self): price = self.total_purchase_with_tax if price != DECIMAL_ZERO: return mark_safe(_("<b>%(price)s</b>") % {"price": price}) return EMPTY_STRING get_html_producer_price_purchased.short_description = _( "Producer amount invoiced") get_html_producer_price_purchased.admin_order_field = "total_purchase_with_tax" def get_html_like(self, user): if settings.REPANIER_SETTINGS_TEMPLATE == "bs3": return mark_safe( '<span class="glyphicon glyphicon-heart{}" onclick="like_ajax({});return false;"></span>' .format( EMPTY_STRING if self.product.likes.filter( id=user.id).only("id").exists() else "-empty", self.id, )) else: return mark_safe( '<span class="fa{} fa-heart" onclick="like_ajax({});return false;"></span>' .format( "s" if self.product.likes.filter( id=user.id).only("id").exists() else "r", self.id, )) def get_order_name(self): qty_display = self.get_qty_display() if qty_display: return "{} {}".format( self.safe_translation_getter("long_name", any_language=True), qty_display, ) return "{}".format( self.safe_translation_getter("long_name", any_language=True)) def get_qty_display(self): if self.is_box: # To avoid unicode error in email_offer.send_open_order qty_display = BOX_UNICODE else: if self.use_order_unit_converted: # The only conversion done in permanence concerns PRODUCT_ORDER_UNIT_PC_KG # so we are sure that self.order_unit == PRODUCT_ORDER_UNIT_PC_KG qty_display = self.get_display( qty=1, order_unit=PRODUCT_ORDER_UNIT_KG, for_customer=False, without_price_display=True, ) else: qty_display = self.get_display( qty=1, order_unit=self.order_unit, for_customer=False, without_price_display=True, ) return qty_display def get_long_name(self, customer_price=True, is_html=False): return super(OfferItem, self).get_long_name(customer_price=customer_price) def get_html_long_name(self): return mark_safe(self.get_long_name(is_html=True)) def get_long_name_with_producer(self, is_html=False): return super(OfferItem, self).get_long_name_with_producer() def get_html_long_name_with_producer(self): return mark_safe(self.get_long_name_with_producer(is_html=True)) get_html_long_name_with_producer.short_description = _("Offer items") get_html_long_name_with_producer.admin_order_field = "translations__long_name" def __str__(self): return self.get_long_name_with_producer() class Meta: verbose_name = _("Offer item") verbose_name_plural = _("Offer items") unique_together = ("permanence", "product")
class Customer(models.Model): user = models.OneToOneField(settings.AUTH_USER_MODEL) login_attempt_counter = models.DecimalField( _("login attempt counter"), default=DECIMAL_ZERO, max_digits=2, decimal_places=0) short_basket_name = models.CharField( _("Short name"), max_length=25, null=False, default=EMPTY_STRING, db_index=True, unique=True) long_basket_name = models.CharField( _("Long name"), max_length=100, null=True, default=EMPTY_STRING) email2 = models.EmailField( _("secondary email"), null=True, blank=True, default=EMPTY_STRING) language = models.CharField( max_length=5, choices=settings.LANGUAGES, default=settings.LANGUAGE_CODE, verbose_name=_("language")) picture = AjaxPictureField( verbose_name=_("picture"), null=True, blank=True, upload_to="customer", size=SIZE_S) phone1 = models.CharField( _("phone1"), max_length=25, null=True, blank=True, default=EMPTY_STRING) phone2 = models.CharField( _("phone2"), max_length=25, null=True, blank=True, default=EMPTY_STRING) bank_account1 = models.CharField(_("main bank account"), max_length=100, null=True, blank=True, default=EMPTY_STRING) bank_account2 = models.CharField(_("secondary bank account"), max_length=100, null=True, blank=True, default=EMPTY_STRING) vat_id = models.CharField( _("vat_id"), max_length=20, null=True, blank=True, default=EMPTY_STRING) address = models.TextField( _("address"), null=True, blank=True, default=EMPTY_STRING) city = models.CharField( _("city"), max_length=50, null=True, blank=True, default=EMPTY_STRING) about_me = models.TextField( _("about me"), null=True, blank=True, default=EMPTY_STRING) memo = models.TextField( _("memo"), null=True, blank=True, default=EMPTY_STRING) accept_mails_from_members = models.BooleanField( _("show my mail to other members"), default=False) accept_phone_call_from_members = models.BooleanField( _("show my phone to other members"), default=False) membership_fee_valid_until = models.DateField( _("membership fee valid until"), default=datetime.date.today ) # If this customer is member of a closed group, the customer.price_list_multiplier is not used # Invoices are sent to the consumer responsible of the group who is # also responsible for collecting the payments. # The LUT_DeliveryPoint.price_list_multiplier will be used when invoicing the consumer responsible # At this stage, the link between the customer invoice and this customer responsible is made with # CustomerInvoice.customer_charged price_list_multiplier = models.DecimalField( _("Customer price list multiplier"), help_text=_("This multiplier is applied to each product ordered by this customer."), default=DECIMAL_ONE, max_digits=5, decimal_places=4, blank=True, validators=[MinValueValidator(0)]) password_reset_on = models.DateTimeField( _("password_reset_on"), null=True, blank=True, default=None) date_balance = models.DateField( _("date_balance"), default=datetime.date.today) balance = ModelMoneyField( _("balance"), max_digits=8, decimal_places=2, default=DECIMAL_ZERO) # The initial balance is needed to compute the invoice control list initial_balance = ModelMoneyField( _("initial balance"), max_digits=8, decimal_places=2, default=DECIMAL_ZERO) represent_this_buyinggroup = models.BooleanField( _("represent_this_buyinggroup"), default=False) delivery_point = models.ForeignKey( 'LUT_DeliveryPoint', verbose_name=_("delivery point"), blank=True, null=True, default=None) is_active = models.BooleanField(_("is_active"), default=True) is_group = models.BooleanField(_("is a group"), default=False) may_order = models.BooleanField(_("may_order"), default=True) valid_email = models.NullBooleanField(_("valid_email"), default=None) subscribe_to_email = models.BooleanField(_("subscribe to email"), default=True) preparation_order = models.IntegerField(null=True, blank=True, default=0) def get_admin_date_balance(self): return timezone.now().date().strftime(settings.DJANGO_SETTINGS_DATE) get_admin_date_balance.short_description = (_("date_balance")) get_admin_date_balance.allow_tags = False def get_admin_date_joined(self): return self.user.date_joined.strftime(settings.DJANGO_SETTINGS_DATE) get_admin_date_joined.short_description = _("date joined") get_admin_date_joined.allow_tags = False def get_admin_balance(self): return self.balance + self.get_bank_not_invoiced() - self.get_order_not_invoiced() get_admin_balance.short_description = (_("balance")) get_admin_balance.allow_tags = False def get_order_not_invoiced(self): from repanier.apps import REPANIER_SETTINGS_INVOICE if REPANIER_SETTINGS_INVOICE: result_set = CustomerInvoice.objects.filter( customer_id=self.id, status__gte=PERMANENCE_OPENED, status__lte=PERMANENCE_SEND, customer_charged_id=self.id ).order_by('?').aggregate(Sum('total_price_with_tax'), Sum('delta_price_with_tax'), Sum('delta_transport')) if result_set["total_price_with_tax__sum"] is not None: order_not_invoiced = RepanierMoney(result_set["total_price_with_tax__sum"]) else: order_not_invoiced = REPANIER_MONEY_ZERO if result_set["delta_price_with_tax__sum"] is not None: order_not_invoiced += RepanierMoney(result_set["delta_price_with_tax__sum"]) if result_set["delta_transport__sum"] is not None: order_not_invoiced += RepanierMoney(result_set["delta_transport__sum"]) else: order_not_invoiced = REPANIER_MONEY_ZERO return order_not_invoiced def get_bank_not_invoiced(self): from repanier.apps import REPANIER_SETTINGS_INVOICE if REPANIER_SETTINGS_INVOICE: result_set = BankAccount.objects.filter( customer_id=self.id, customer_invoice__isnull=True ).order_by('?').aggregate(Sum('bank_amount_in'), Sum('bank_amount_out')) if result_set["bank_amount_in__sum"] is not None: bank_in = RepanierMoney(result_set["bank_amount_in__sum"]) else: bank_in = REPANIER_MONEY_ZERO if result_set["bank_amount_out__sum"] is not None: bank_out = RepanierMoney(result_set["bank_amount_out__sum"]) else: bank_out = REPANIER_MONEY_ZERO bank_not_invoiced = bank_in - bank_out else: bank_not_invoiced = REPANIER_MONEY_ZERO return bank_not_invoiced def get_balance(self): last_customer_invoice = CustomerInvoice.objects.filter( customer_id=self.id, invoice_sort_order__isnull=False ).order_by('?') balance = self.get_admin_balance() if last_customer_invoice.exists(): if balance.amount >= 30: return '<a href="' + urlresolvers.reverse('customer_invoice_view', args=(0,)) + '?customer=' + str( self.id) + '" class="btn" target="_blank" >' + ( '<span style="color:#32CD32">%s</span>' % (balance,)) + '</a>' elif balance.amount >= -10: return '<a href="' + urlresolvers.reverse('customer_invoice_view', args=(0,)) + '?customer=' + str( self.id) + '" class="btn" target="_blank" >' + ( '<span style="color:#696969">%s</span>' % (balance,)) + '</a>' else: return '<a href="' + urlresolvers.reverse('customer_invoice_view', args=(0,)) + '?customer=' + str( self.id) + '" class="btn" target="_blank" >' + ( '<span style="color:red">%s</span>' % (balance,)) + '</a>' else: if balance.amount >= 30: return '<span style="color:#32CD32">%s</span>' % (balance,) elif balance.amount >= -10: return '<span style="color:#696969">%s</span>' % (balance,) else: return '<span style="color:red">%s</span>' % (balance,) get_balance.short_description = _("balance") get_balance.allow_tags = True get_balance.admin_order_field = 'balance' def get_on_hold_movement_html(self, bank_not_invoiced=None, order_not_invoiced=None, total_price_with_tax=REPANIER_MONEY_ZERO): from repanier.apps import REPANIER_SETTINGS_INVOICE if REPANIER_SETTINGS_INVOICE: bank_not_invoiced = bank_not_invoiced if bank_not_invoiced is not None else self.get_bank_not_invoiced() order_not_invoiced = order_not_invoiced if order_not_invoiced is not None else self.get_order_not_invoiced() other_order_not_invoiced = order_not_invoiced - total_price_with_tax else: bank_not_invoiced = REPANIER_MONEY_ZERO other_order_not_invoiced = REPANIER_MONEY_ZERO if other_order_not_invoiced.amount != DECIMAL_ZERO or bank_not_invoiced.amount != DECIMAL_ZERO: if other_order_not_invoiced.amount != DECIMAL_ZERO: if bank_not_invoiced.amount == DECIMAL_ZERO: customer_on_hold_movement = \ _('This balance does not take account of any unbilled sales %(other_order)s.') % { 'other_order': other_order_not_invoiced } else: customer_on_hold_movement = \ _( 'This balance does not take account of any unrecognized payments %(bank)s and any unbilled order %(other_order)s.') \ % { 'bank' : bank_not_invoiced, 'other_order': other_order_not_invoiced } else: customer_on_hold_movement = \ _( 'This balance does not take account of any unrecognized payments %(bank)s.') % { 'bank': bank_not_invoiced } customer_on_hold_movement = mark_safe(customer_on_hold_movement) else: customer_on_hold_movement = EMPTY_STRING return customer_on_hold_movement def get_last_membership_fee(self): from repanier.models.purchase import Purchase last_membership_fee = Purchase.objects.filter( customer_id=self.id, offer_item__order_unit=PRODUCT_ORDER_UNIT_MEMBERSHIP_FEE ).order_by("-id") if last_membership_fee.exists(): return last_membership_fee.first().selling_price get_last_membership_fee.short_description = _("last membership fee") get_last_membership_fee.allow_tags = False def last_membership_fee_date(self): from repanier.models.purchase import Purchase last_membership_fee = Purchase.objects.filter( customer_id=self.id, offer_item__order_unit=PRODUCT_ORDER_UNIT_MEMBERSHIP_FEE ).order_by("-id").prefetch_related("customer_invoice") if last_membership_fee.exists(): return last_membership_fee.first().customer_invoice.date_balance last_membership_fee_date.short_description = _("last membership fee date") last_membership_fee_date.allow_tags = False def get_last_membership_fee_date(self): # Format it for the admin # Don't format it form import/export last_membership_fee_date = self.last_membership_fee_date() if last_membership_fee_date is not None: return last_membership_fee_date.strftime(settings.DJANGO_SETTINGS_DATE) return EMPTY_STRING get_last_membership_fee_date.short_description = _("last membership fee date") get_last_membership_fee_date.allow_tags = False def get_participation(self): now = timezone.now() return PermanenceBoard.objects.filter( customer_id=self.id, permanence_date__gte=now - datetime.timedelta(days=ONE_YEAR), permanence_date__lt=now, permanence_role__is_counted_as_participation=True ).order_by('?').count() get_participation.short_description = _("participation") get_participation.allow_tags = False def get_purchase(self): now = timezone.now() # Do not count invoice having only products free of charge return CustomerInvoice.objects.filter( customer_id=self.id, total_price_with_tax__gt=DECIMAL_ZERO, date_balance__gte=now - datetime.timedelta(ONE_YEAR) ).count() get_purchase.short_description = _("purchase") get_purchase.allow_tags = False def my_order_confirmation_email_send_to(self): from repanier.apps import REPANIER_SETTINGS_CUSTOMERS_MUST_CONFIRM_ORDERS if self.email2: to_email = (self.user.email, self.email2) else: to_email = (self.user.email,) sent_to = ", ".join(to_email) if to_email is not None else EMPTY_STRING if REPANIER_SETTINGS_CUSTOMERS_MUST_CONFIRM_ORDERS: msg_confirmation = _( "Your order is confirmed. An email containing this order summary has been sent to %s.") % sent_to else: msg_confirmation = _("An email containing this order summary has been sent to %s.") % sent_to return msg_confirmation @property def who_is_who_display(self): return self.picture or self.accept_mails_from_members or self.accept_phone_call_from_members \ or (self.about_me is not None and len(self.about_me.strip()) > 1) def get_unsubscribe_mail_footer(self): return '<br/><br/><hr/><br/><a href="%s">%s</a>' % (self._get_unsubscribe_link(), _("Unsubscribe to emails")) def _get_unsubscribe_link(self): customer_id, token = self.make_token().split(":", 1) return "https://%s%s" % ( settings.ALLOWED_HOSTS[0], reverse( 'unsubscribe_view', kwargs={'customer_id': customer_id, 'token': token, } ) ) def make_token(self): return TimestampSigner().sign(self.id) def check_token(self, token): try: key = '%s:%s' % (self.id, token) TimestampSigner().unsign(key, max_age=60 * 60 * 48) # Valid for 2 days except (BadSignature, SignatureExpired): return False return True def __str__(self): return self.short_basket_name class Meta: verbose_name = _("customer") verbose_name_plural = _("customers") ordering = ("short_basket_name",) index_together = [ ["user", "is_active", "may_order"], ]
class Producer(models.Model): short_profile_name = models.CharField( _("Short name"), max_length=25, blank=False, default=EMPTY_STRING, db_index=True, unique=True, ) long_profile_name = models.CharField(_("Long name"), max_length=100, blank=True, default=EMPTY_STRING) email = models.EmailField(_("Email"), null=True, blank=True, default=EMPTY_STRING) email2 = models.EmailField(_("Secondary email"), null=True, blank=True, default=EMPTY_STRING) email3 = models.EmailField(_("Secondary email"), null=True, blank=True, default=EMPTY_STRING) language = models.CharField( max_length=5, choices=settings.LANGUAGES, default=settings.LANGUAGE_CODE, verbose_name=_("Language"), ) picture = RepanierPictureField( verbose_name=_("Picture"), null=True, blank=True, upload_to="producer", size=SIZE_L, ) phone1 = models.CharField(_("Phone1"), max_length=25, blank=True, default=EMPTY_STRING) phone2 = models.CharField(_("Phone2"), max_length=25, blank=True, default=EMPTY_STRING) bank_account = models.CharField(_("Bank account"), max_length=100, blank=True, default=EMPTY_STRING) vat_id = models.CharField(_("VAT id"), max_length=20, blank=True, default=EMPTY_STRING) fax = models.CharField(_("Fax"), max_length=100, blank=True, default=EMPTY_STRING) address = models.TextField(_("Address"), blank=True, default=EMPTY_STRING) city = models.CharField(_("City"), max_length=50, blank=True, default=EMPTY_STRING) memo = models.TextField(_("Memo"), blank=True, default=EMPTY_STRING) reference_site = models.URLField(_("Reference site"), null=True, blank=True, default=EMPTY_STRING) web_services_activated = models.BooleanField(_("Web services activated"), default=False) # uuid used to access to producer invoices without login uuid = models.CharField("uuid", max_length=36, default=EMPTY_STRING, db_index=True) offer_uuid = models.CharField("uuid", max_length=36, default=EMPTY_STRING, db_index=True) offer_filled = models.BooleanField(_("Offer filled"), default=False) invoice_by_basket = models.BooleanField(_("Invoice by basket"), default=False) producer_price_are_wo_vat = models.BooleanField( _("Producer price are wo vat"), default=False) sort_products_by_reference = models.BooleanField( _("Sort products by reference"), default=False) price_list_multiplier = models.DecimalField( _("Coefficient applied to the producer tariff to calculate the consumer tariff" ), help_text= _("This multiplier is applied to each price automatically imported/pushed." ), default=DECIMAL_ONE, max_digits=5, decimal_places=4, blank=True, validators=[MinValueValidator(0)], ) minimum_order_value = ModelMoneyField( _("Minimum order value"), help_text=_("0 mean : no minimum order value."), max_digits=8, decimal_places=2, default=DECIMAL_ZERO, validators=[MinValueValidator(0)], ) date_balance = models.DateField(_("Date_balance"), default=datetime.date.today) balance = ModelMoneyField(_("Balance"), max_digits=8, decimal_places=2, default=DECIMAL_ZERO) initial_balance = ModelMoneyField(_("Initial balance"), max_digits=8, decimal_places=2, default=DECIMAL_ZERO) represent_this_buyinggroup = models.BooleanField( _("Represent this buyinggroup"), default=False) is_active = models.BooleanField(_("Active"), default=True) # This indicate that the user record data have been replaced with anonymous data in application of GDPR is_anonymized = models.BooleanField(default=False) @classmethod def get_or_create_group(cls): producer_buyinggroup = (Producer.objects.filter( represent_this_buyinggroup=True).order_by("?").first()) if producer_buyinggroup is None: long_name = settings.REPANIER_SETTINGS_GROUP_NAME short_name = long_name[:25] producer_buyinggroup = Producer.objects.create( short_profile_name=short_name, long_profile_name=long_name, phone1=settings.REPANIER_SETTINGS_COORDINATOR_PHONE, represent_this_buyinggroup=True, ) # Create this to also prevent the deletion of the producer representing the buying group membership_fee_product = Product.objects.filter( order_unit=PRODUCT_ORDER_UNIT_MEMBERSHIP_FEE, is_active=True).order_by("?") if not membership_fee_product.exists(): membership_fee_product = Product.objects.create( producer_id=producer_buyinggroup.id, order_unit=PRODUCT_ORDER_UNIT_MEMBERSHIP_FEE, vat_level=VAT_100, ) cur_language = translation.get_language() for language in settings.PARLER_LANGUAGES[settings.SITE_ID]: language_code = language["code"] translation.activate(language_code) membership_fee_product.set_current_language(language_code) membership_fee_product.long_name = "{}".format( _("Membership fee")) membership_fee_product.save() translation.activate(cur_language) return producer_buyinggroup def get_phone1(self, prefix=EMPTY_STRING): # return ", phone1" if prefix = ", " if not self.phone1: return EMPTY_STRING return "{}{}".format(prefix, self.phone1) def get_phone2(self): return self.phone2 or EMPTY_STRING def get_negative_balance(self): return -self.balance def get_products(self): # This producer may have product's list if self.is_active: changeproductslist_url = reverse( "admin:repanier_product_changelist") link = '<a href="{}?is_active__exact=1&producer={}" class="repanier-a-info"> {}</a>'.format( changeproductslist_url, str(self.id), _("Products")) return format_html(link) return EMPTY_STRING get_products.short_description = EMPTY_STRING def get_admin_date_balance(self): return timezone.now().strftime(settings.DJANGO_SETTINGS_DATETIME) get_admin_date_balance.short_description = _("Date balance") def get_admin_balance(self): return (self.balance - self.get_bank_not_invoiced() + self.get_order_not_invoiced()) get_admin_balance.short_description = _("Balance") def get_order_not_invoiced(self): if settings.REPANIER_SETTINGS_MANAGE_ACCOUNTING: result_set = (ProducerInvoice.objects.filter( producer_id=self.id, status__gte=PERMANENCE_OPENED, status__lte=PERMANENCE_SEND, ).order_by("?").aggregate( total_price_with_tax=Sum( "total_price_with_tax", output_field=DecimalField(max_digits=8, decimal_places=2, default=DECIMAL_ZERO), ), delta_price_with_tax=Sum( "delta_price_with_tax", output_field=DecimalField(max_digits=8, decimal_places=2, default=DECIMAL_ZERO), ), delta_transport=Sum( "delta_transport", output_field=DecimalField(max_digits=5, decimal_places=2, default=DECIMAL_ZERO), ), )) if result_set["total_price_with_tax"] is not None: order_not_invoiced = RepanierMoney( result_set["total_price_with_tax"]) else: order_not_invoiced = REPANIER_MONEY_ZERO if result_set["delta_price_with_tax"] is not None: order_not_invoiced += RepanierMoney( result_set["delta_price_with_tax"]) if result_set["delta_transport"] is not None: order_not_invoiced += RepanierMoney( result_set["delta_transport"]) else: order_not_invoiced = REPANIER_MONEY_ZERO return order_not_invoiced def get_bank_not_invoiced(self): if settings.REPANIER_SETTINGS_MANAGE_ACCOUNTING: result_set = (BankAccount.objects.filter( producer_id=self.id, producer_invoice__isnull=True).order_by("?").aggregate( bank_amount_in=Sum( "bank_amount_in", output_field=DecimalField(max_digits=8, decimal_places=2, default=DECIMAL_ZERO), ), bank_amount_out=Sum( "bank_amount_out", output_field=DecimalField(max_digits=8, decimal_places=2, default=DECIMAL_ZERO), ), )) total_bank_amount_in = (result_set["bank_amount_in"] if result_set["bank_amount_in"] is not None else DECIMAL_ZERO) total_bank_amount_out = (result_set["bank_amount_out"] if result_set["bank_amount_out"] is not None else DECIMAL_ZERO) bank_not_invoiced = RepanierMoney(total_bank_amount_out - total_bank_amount_in) else: bank_not_invoiced = REPANIER_MONEY_ZERO return bank_not_invoiced def get_calculated_invoiced_balance(self, permanence_id): bank_not_invoiced = self.get_bank_not_invoiced() # Do not take into account product whose order unit is >= PRODUCT_ORDER_UNIT_DEPOSIT result_set = (OfferItemWoReceiver.objects.filter( permanence_id=permanence_id, producer_id=self.id, price_list_multiplier__lt=1, ).exclude(order_unit__gte=PRODUCT_ORDER_UNIT_DEPOSIT).order_by( "?").aggregate(total_selling_price_with_tax=Sum( "total_selling_with_tax", output_field=DecimalField( max_digits=8, decimal_places=2, default=DECIMAL_ZERO), ))) payment_needed = (result_set["total_selling_price_with_tax"] if result_set["total_selling_price_with_tax"] is not None else DECIMAL_ZERO) result_set = (OfferItemWoReceiver.objects.filter( permanence_id=permanence_id, producer_id=self.id, price_list_multiplier__gte=1, ).exclude(order_unit__gte=PRODUCT_ORDER_UNIT_DEPOSIT).order_by( "?").aggregate(total_purchase_price_with_tax=Sum( "total_purchase_with_tax", output_field=DecimalField( max_digits=8, decimal_places=2, default=DECIMAL_ZERO), ))) if result_set["total_purchase_price_with_tax"] is not None: payment_needed += result_set["total_purchase_price_with_tax"] calculated_invoiced_balance = self.balance - bank_not_invoiced + payment_needed return calculated_invoiced_balance get_calculated_invoiced_balance.short_description = _("Balance") def get_balance(self): last_producer_invoice_set = ProducerInvoice.objects.filter( producer_id=self.id, invoice_sort_order__isnull=False).order_by("?") balance = self.get_admin_balance() if balance.amount < 0: color = "#298A08" elif balance.amount == 0: color = "#32CD32" elif balance.amount > 30: color = "red" else: color = "#696969" if last_producer_invoice_set.exists(): return format_html( '<a href="{}?producer={}" class="repanier-a-info" target="_blank"><span style="color:{}">{}</span></a>', reverse("producer_invoice_view", args=(0, )), str(self.id), color, -balance, ) else: return format_html('<span style="color:{}">{}</span>', color, -balance) get_balance.short_description = _("Balance") get_balance.allow_tags = True get_balance.admin_order_field = "balance" def get_last_invoice(self): producer_last_invoice = (ProducerInvoice.objects.filter( producer_id=self.id, invoice_sort_order__isnull=False).order_by("-id").first()) if producer_last_invoice is not None: total_price_with_tax = producer_last_invoice.get_total_price_with_tax( ) if total_price_with_tax < DECIMAL_ZERO: return format_html( '<span style="color:#298A08">{}</span>', number_format(total_price_with_tax, 2), ) elif total_price_with_tax == DECIMAL_ZERO: return format_html( '<span style="color:#32CD32">{}</span>', number_format(total_price_with_tax, 2), ) elif total_price_with_tax > 30: return format_html( '<span style="color:red">{}</span>', number_format(total_price_with_tax, 2), ) else: return format_html( '<span style="color:#696969">{}</span>', number_format(total_price_with_tax, 2), ) else: return format_html('<span style="color:#32CD32">{}</span>', number_format(0, 2)) get_last_invoice.short_description = _("Last invoice") def get_html_on_hold_movement(self): bank_not_invoiced = self.get_bank_not_invoiced() order_not_invoiced = self.get_order_not_invoiced() if (order_not_invoiced.amount != DECIMAL_ZERO or bank_not_invoiced.amount != DECIMAL_ZERO): if order_not_invoiced.amount != DECIMAL_ZERO: if bank_not_invoiced.amount == DECIMAL_ZERO: producer_on_hold_movement = _( "This balance does not take account of any unbilled sales %(other_order)s." ) % { "other_order": order_not_invoiced } else: producer_on_hold_movement = _( "This balance does not take account of any unrecognized payments %(bank)s and any unbilled order %(other_order)s." ) % { "bank": bank_not_invoiced, "other_order": order_not_invoiced } else: producer_on_hold_movement = _( "This balance does not take account of any unrecognized payments %(bank)s." ) % { "bank": bank_not_invoiced } return mark_safe(producer_on_hold_movement) return EMPTY_STRING def anonymize(self, also_group=False): if self.represent_this_buyinggroup: if not also_group: return self.short_profile_name = "{}-{}".format(_("GROUP"), self.id) self.long_profile_name = "{} {}".format(_("Group"), self.id) else: self.short_profile_name = "{}-{}".format(_("PRODUCER"), self.id) self.long_profile_name = "{} {}".format(_("Producer"), self.id) self.email = "{}@repanier.be".format(self.short_profile_name) self.email2 = EMPTY_STRING self.email3 = EMPTY_STRING self.phone1 = EMPTY_STRING self.phone2 = EMPTY_STRING self.bank_account = EMPTY_STRING self.vat_id = EMPTY_STRING self.fax = EMPTY_STRING self.address = EMPTY_STRING self.memo = EMPTY_STRING self.uuid = uuid.uuid1() self.offer_uuid = uuid.uuid1() self.is_anonymized = True self.save() def __str__(self): if self.producer_price_are_wo_vat: return "{} {}".format(self.short_profile_name, _("wo VAT")) return self.short_profile_name class Meta: verbose_name = _("Producer") verbose_name_plural = _("Producers") ordering = ("-represent_this_buyinggroup", "short_profile_name") indexes = [ models.Index( fields=["-represent_this_buyinggroup", "short_profile_name"], name="producer_order_idx", ) ]
class ProducerInvoice(Invoice): producer = models.ForeignKey( 'Producer', verbose_name=_("Producer"), # related_name='producer_invoice', on_delete=models.PROTECT) delta_stock_with_tax = ModelMoneyField(_("Amount deducted from the stock"), default=DECIMAL_ZERO, max_digits=8, decimal_places=2) delta_stock_vat = ModelMoneyField(_("Total VAT deducted from the stock"), default=DECIMAL_ZERO, max_digits=9, decimal_places=4) delta_deposit = ModelMoneyField(_("Deposit"), help_text=_('+ Deposit'), default=DECIMAL_ZERO, max_digits=8, decimal_places=2) delta_stock_deposit = ModelMoneyField(_("Deposit"), help_text=_('+ Deposit'), default=DECIMAL_ZERO, max_digits=8, decimal_places=2) to_be_paid = models.BooleanField(_("To be paid"), choices=LUT_BANK_NOTE, default=False) calculated_invoiced_balance = ModelMoneyField( _("Amount due to the producer as calculated by Repanier"), max_digits=8, decimal_places=2, default=DECIMAL_ZERO) to_be_invoiced_balance = ModelMoneyField( _("Amount claimed by the producer"), max_digits=8, decimal_places=2, default=DECIMAL_ZERO) invoice_sort_order = models.IntegerField(_("Invoice sort order"), default=None, blank=True, null=True, db_index=True) invoice_reference = models.CharField(_("Invoice reference"), max_length=100, null=True, blank=True) def get_negative_previous_balance(self): return -self.previous_balance def get_negative_balance(self): return -self.balance def get_total_price_with_tax(self): return self.total_price_with_tax + self.delta_price_with_tax + self.delta_transport + self.delta_stock_with_tax def get_total_vat(self): return self.total_vat + self.delta_stock_vat def get_total_deposit(self): return self.total_deposit + self.delta_stock_deposit def get_order_json(self): a_producer = self.producer json_dict = {} if a_producer.minimum_order_value.amount > DECIMAL_ZERO: ratio = self.total_price_with_tax.amount / a_producer.minimum_order_value.amount if ratio >= DECIMAL_ONE: ratio = 100 else: ratio *= 100 json_dict["#order_procent{}".format(a_producer.id)] = "{}%".format( number_format(ratio, 0)) if self.status != PERMANENCE_OPENED: json_dict["#order_closed{}".format(a_producer.id)] = mark_safe( " <span class=\"glyphicon glyphicon-ban-circle\" aria-hidden=\"true\"></span>" ) return json_dict def __str__(self): return "{}, {}".format(self.producer, self.permanence) class Meta: verbose_name = _("Producer invoice") verbose_name_plural = _("Producers invoices") unique_together = ( "permanence", "producer", )
class Permanence(TranslatableModel): translations = TranslatedFields( short_name=models.CharField( _("offer name"), max_length=50, blank=True ), offer_description=HTMLField( _("offer_description"), configuration='CKEDITOR_SETTINGS_MODEL2', help_text=_( "This message is send by mail to all customers when opening the order or on top "), blank=True, default=EMPTY_STRING ), invoice_description=HTMLField( _("invoice_description"), configuration='CKEDITOR_SETTINGS_MODEL2', help_text=_( 'This message is send by mail to all customers having bought something when closing the permanence.'), blank=True, default=EMPTY_STRING ), ) status = models.CharField( _("Status"), max_length=3, choices=LUT_PERMANENCE_STATUS, default=PERMANENCE_PLANNED, ) permanence_date = models.DateField( _("Date"), db_index=True ) payment_date = models.DateField( _("Payment date"), blank=True, null=True, db_index=True ) producers = models.ManyToManyField( 'Producer', verbose_name=_("Producers"), blank=True ) boxes = models.ManyToManyField( 'Box', verbose_name=_('Boxes'), blank=True ) contracts = models.ManyToManyField( 'Contract', verbose_name=_("Commitments"), blank=True ) # When master contract is defined, this permanence is used to let customer place order to the contract # This master contract will be the only one contract allowed for this permanence # This permanence will not be showed into "admin/permanence_in_preparation" nor "admin/ermanence_done" # but will be used in "admin/contract" master_contract = models.OneToOneField( 'Contract', related_name='master_contract', verbose_name=_("Master contract"), on_delete=models.CASCADE, null=True, blank=True, default=None) # Calculated with Purchase total_purchase_with_tax = ModelMoneyField( _("Total amount"), help_text=_('Total purchase amount vat included'), default=DECIMAL_ZERO, max_digits=8, decimal_places=2) total_selling_with_tax = ModelMoneyField( _("Total amount"), help_text=_('Total purchase amount vat included'), default=DECIMAL_ZERO, max_digits=8, decimal_places=2) total_purchase_vat = ModelMoneyField( _("Total vat"), help_text=_('Vat part of the total purchased'), default=DECIMAL_ZERO, max_digits=9, decimal_places=4) total_selling_vat = ModelMoneyField( _("Total vat"), help_text=_('Vat part of the total purchased'), default=DECIMAL_ZERO, max_digits=9, decimal_places=4) with_delivery_point = models.BooleanField( _("With delivery point"), default=False) automatically_closed = models.BooleanField( _("Automatically closed"), default=False) is_updated_on = models.DateTimeField( _("Is updated on"), auto_now=True) highest_status = models.CharField( max_length=3, choices=LUT_PERMANENCE_STATUS, default=PERMANENCE_PLANNED, verbose_name=_("highest permanence_status"), ) master_permanence = models.ForeignKey( 'Permanence', verbose_name=_("Master permanence"), related_name='child_permanence', blank=True, null=True, default=None, on_delete=models.PROTECT, db_index=True) invoice_sort_order = models.IntegerField( _("Invoice sort order"), default=None, blank=True, null=True) offer_description_on_home_page = models.BooleanField( _("Publish the offer description on the home page when the permanence is open"), default=True) picture = AjaxPictureField( verbose_name=_("picture"), null=True, blank=True, upload_to="permanence", size=SIZE_L) gauge = models.IntegerField( default=0, editable=False ) @cached_property def get_producers(self): if self.status == PERMANENCE_PLANNED: if len(self.producers.all()) > 0: changelist_url = urlresolvers.reverse( 'admin:repanier_product_changelist', ) link = [] for p in self.producers.all(): link.append( '<a href="%s?producer=%d"> %s</a>' % ( changelist_url, p.id, p.short_profile_name)) msg_html = '<div class="wrap-text">%s</div>' % ", ".join(link) else: msg_html = '<div class="wrap-text">%s</div>' % _("No offer") elif self.status == PERMANENCE_PRE_OPEN: msg_html = '<div class="wrap-text">%s</div>' % ", ".join([p.short_profile_name + " (" + p.phone1 + ")" for p in self.producers.all()]) elif self.status in [PERMANENCE_OPENED, PERMANENCE_CLOSED]: close_offeritem_changelist_url = urlresolvers.reverse( 'admin:repanier_offeritemclosed_changelist', ) link = [] for p in self.producers.all().only("id"): pi = ProducerInvoice.objects.filter( producer_id=p.id, permanence_id=self.id ).order_by('?').first() if pi is not None: if pi.status == PERMANENCE_OPENED: label = ('%s (%s) ' % (p.short_profile_name, pi.get_total_price_with_tax())).replace( ' ', ' ') offeritem_changelist_url = close_offeritem_changelist_url else: label = ('%s (%s) %s' % ( p.short_profile_name, pi.get_total_price_with_tax(), LOCK_UNICODE)).replace(' ', ' ') offeritem_changelist_url = close_offeritem_changelist_url else: label = ('%s ' % (p.short_profile_name,)).replace(' ', ' ') offeritem_changelist_url = close_offeritem_changelist_url link.append( '<a href="%s?permanence=%s&producer=%d">%s</a>' % ( offeritem_changelist_url, self.id, p.id, label)) msg_html = '<div class="wrap-text">%s</div>' % ", ".join(link) elif self.status in [PERMANENCE_SEND, PERMANENCE_INVOICED, PERMANENCE_ARCHIVED]: send_offeritem_changelist_url = urlresolvers.reverse( 'admin:repanier_offeritemsend_changelist', ) send_customer_changelist_url = urlresolvers.reverse( 'admin:repanier_customersend_changelist', ) link = [] at_least_one_permanence_send = False for pi in ProducerInvoice.objects.filter(permanence_id=self.id).select_related( "producer").order_by('producer'): if pi.status == PERMANENCE_SEND: at_least_one_permanence_send = True if pi.producer.invoice_by_basket: changelist_url = send_customer_changelist_url else: changelist_url = send_offeritem_changelist_url # Important : no target="_blank" label = '%s (%s) %s' % ( pi.producer.short_profile_name, pi.get_total_price_with_tax(), LOCK_UNICODE) link.append( '<a href="%s?permanence=%d&producer=%d"> %s</a>' % ( changelist_url, self.id, pi.producer_id, label.replace(' ', ' ') )) else: if pi.invoice_reference: if pi.to_be_invoiced_balance != DECIMAL_ZERO or pi.total_price_with_tax != DECIMAL_ZERO: label = "%s (%s - %s)" % ( pi.producer.short_profile_name, pi.to_be_invoiced_balance, cap(pi.invoice_reference, 15) ) else: label = "%s (%s)" % ( pi.producer.short_profile_name, cap(pi.invoice_reference, 15) ) else: if pi.to_be_invoiced_balance != DECIMAL_ZERO or pi.total_price_with_tax != DECIMAL_ZERO: label = "%s (%s)" % ( pi.producer.short_profile_name, pi.to_be_invoiced_balance ) else: # label = "%s" % ( # pi.producer.short_profile_name # ) continue # Important : target="_blank" because the invoices must be displayed without the cms_toolbar # Such that they can be accessed by the producer and by the staff link.append( '<a href="%s?producer=%d" target="_blank">%s</a>' % ( urlresolvers.reverse('producer_invoice_view', args=(pi.id,)), pi.producer_id, label.replace(' ', ' '))) producers = ", ".join(link) if at_least_one_permanence_send: msg_html = '<div class="wrap-text">%s</div>' % producers else: msg_html = """ <div class="wrap-text"><button onclick="django.jQuery('#id_get_producers_%d').toggle(); if(django.jQuery(this).html()=='%s'){ django.jQuery(this).html('%s') }else{ django.jQuery(this).html('%s') }; return false;" >%s</button> <div id="id_get_producers_%d" style="display:none;">%s</div></div> """ % ( self.id, _("Show"), _("Hide"), _("Show"), _("Show"), self.id, producers ) else: msg_html = '<div class="wrap-text">%s</div>' % ", ".join([p.short_profile_name for p in Producer.objects.filter( producerinvoice__permanence_id=self.id).only( 'short_profile_name')]) return mark_safe(msg_html) get_producers.short_description = (_("Offers from")) # get_producers.allow_tags = True @cached_property def get_customers(self): if self.status in [ PERMANENCE_OPENED, PERMANENCE_CLOSED, PERMANENCE_SEND ]: changelist_url = urlresolvers.reverse( 'admin:repanier_purchase_changelist', ) link = [] delivery_save = None for ci in CustomerInvoice.objects.filter(permanence_id=self.id).select_related( "customer").order_by('delivery', 'customer'): if delivery_save != ci.delivery: delivery_save = ci.delivery if ci.delivery is not None: link.append("<br/><b>%s</b>" % ci.delivery.get_delivery_display()) else: link.append("<br/><br/>--") total_price_with_tax = ci.get_total_price_with_tax(customer_charged=True) # if ci.is_order_confirm_send: label = '%s%s (%s) %s%s' % ( "<b><i>" if ci.is_group else EMPTY_STRING, ci.customer.short_basket_name, "-" if ci.is_group or total_price_with_tax == DECIMAL_ZERO else total_price_with_tax, ci.get_is_order_confirm_send_display(), "</i></b>" if ci.is_group else EMPTY_STRING, ) # else: # label = '%s%s (%s) %s%s' % ( # "<b><i>" if ci.is_group else EMPTY_STRING, # ci.customer.short_basket_name, # "-" if ci.is_group or total_price_with_tax == DECIMAL_ZERO else total_price_with_tax, # ci.get_is_order_confirm_send_display(), # "</i></b>" if ci.is_group else EMPTY_STRING, # ) # Important : no target="_blank" link.append( '<a href="%s?permanence=%d&customer=%d">%s</a>' % (changelist_url, self.id, ci.customer_id, label.replace(' ', ' '))) customers = ", ".join(link) elif self.status in [PERMANENCE_INVOICED, PERMANENCE_ARCHIVED]: link = [] delivery_save = None for ci in CustomerInvoice.objects.filter(permanence_id=self.id).select_related( "customer").order_by('delivery', 'customer'): if delivery_save != ci.delivery: delivery_save = ci.delivery if ci.delivery is not None: link.append("<br/><b>%s</b>" % ci.delivery.get_delivery_display()) else: link.append("<br/><br/>--") total_price_with_tax = ci.get_total_price_with_tax(customer_charged=True) label = "%s%s (%s) %s%s" % ( "<b><i>" if ci.is_group else EMPTY_STRING, ci.customer.short_basket_name, "-" if total_price_with_tax == DECIMAL_ZERO else total_price_with_tax, ci.get_is_order_confirm_send_display(), "</i></b>" if ci.is_group else EMPTY_STRING, ) # Important : target="_blank" because the invoices must be displayed without the cms_toolbar # Such that they can be accessed by the customer and by the staff link.append( '<a href="%s?customer=%d" target="_blank">%s</a>' % ( urlresolvers.reverse('customer_invoice_view', args=(ci.id,)), ci.customer_id, label.replace(' ', ' ') ) ) customers = ", ".join(link) else: customers = ", ".join([c.short_basket_name for c in Customer.objects.filter(customerinvoice__permanence_id=self.id).only( 'short_basket_name')]) if len(customers) > 0: msg_html = """ <div class="wrap-text"><button onclick="django.jQuery('#id_get_customers_%d').toggle(); if(django.jQuery(this).html()=='%s'){ django.jQuery(this).html('%s') }else{ django.jQuery(this).html('%s') }; return false;" >%s</button> <div id="id_get_customers_%d" style="display:none;">%s</div></div> """ % ( self.id, _("Show"), _("Hide"), _("Show"), _("Show"), self.id, customers ) return mark_safe(msg_html) else: return mark_safe('<div class="wrap-text">%s</div>' % _("No purchase")) get_customers.short_description = (_("Purchases by")) # get_customers.allow_tags = True @cached_property def get_board(self): permanenceboard_set = PermanenceBoard.objects.filter( permanence=self, permanence_role__rght=F('permanence_role__lft') + 1 ).order_by("permanence_role__tree_id", "permanence_role__lft") first_board = True board = EMPTY_STRING if permanenceboard_set: for permanenceboard_row in permanenceboard_set: r_link = EMPTY_STRING r = permanenceboard_row.permanence_role if r: r_url = urlresolvers.reverse( 'admin:repanier_lut_permanencerole_change', args=(r.id,) ) r_link = '<a href="' + r_url + \ '" target="_blank">' + r.short_name.replace(' ', ' ') + '</a>' c_link = EMPTY_STRING c = permanenceboard_row.customer if c: c_url = urlresolvers.reverse( 'admin:repanier_customer_change', args=(c.id,) ) c_link = ' -> <a href="' + c_url + \ '" target="_blank">' + c.short_basket_name.replace(' ', ' ') + '</a>' if not first_board: board += '<br/>' board += r_link + c_link first_board = False if not first_board: # At least one role is defined in the permanence board msg_html = """ <div class="wrap-text"><button onclick="django.jQuery('#id_get_board_%d').toggle(); if(django.jQuery(this).html()=='%s'){ django.jQuery(this).html('%s') }else{ django.jQuery(this).html('%s') }; return false;" >%s</button> <div id="id_get_board_%d" style="display:none;">%s</div></div> """ % ( self.id, _("Show"), _("Hide"), _("Show"), _("Show"), self.id, board ) return mark_safe(msg_html) else: return mark_safe('<div class="wrap-text">%s</div>' % _("No task")) get_board.short_description = (_("Tasks")) # get_board.allow_tags = True def set_status(self, new_status, all_producers=True, producers_id=None, update_payment_date=False, payment_date=None, allow_downgrade=True): from repanier.models.purchase import Purchase if all_producers: now = timezone.now().date() self.is_updated_on = now self.status = new_status if self.highest_status < new_status: self.highest_status = new_status if update_payment_date: if payment_date is None: self.payment_date = now else: self.payment_date = payment_date self.save( update_fields=['status', 'is_updated_on', 'highest_status', 'payment_date']) else: self.save(update_fields=['status', 'is_updated_on', 'highest_status']) if new_status == PERMANENCE_WAIT_FOR_OPEN: for a_producer in self.producers.all(): # Create ProducerInvoice to be able to close those producer on demand if not ProducerInvoice.objects.filter( permanence_id=self.id, producer_id=a_producer.id ).order_by('?').exists(): ProducerInvoice.objects.create( permanence_id=self.id, producer_id=a_producer.id ) # self.with_delivery_point = DeliveryBoard.objects.filter( # permanence_id=self.id # ).order_by('?').exists() if self.with_delivery_point: qs = DeliveryBoard.objects.filter( permanence_id=self.id ).exclude(status=new_status).order_by('?') for delivery_point in qs: if allow_downgrade or delivery_point.status < new_status: # --> or delivery_point.status < new_status --> # Set new status except if PERMANENCE_SEND, PERMANENCE_WAIT_FOR_SEND # -> PERMANENCE_CLOSED, PERMANENCE_WAIT_FOR_CLOSED # This occur if directly sending order of a opened delivery point delivery_point.set_status(new_status) CustomerInvoice.objects.filter( permanence_id=self.id, delivery__isnull=True ).order_by('?').update( status=new_status ) Purchase.objects.filter( permanence_id=self.id, customer_invoice__delivery__isnull=True ).order_by('?').update( status=new_status) ProducerInvoice.objects.filter( permanence_id=self.id ).order_by('?').update( status=new_status ) if update_payment_date: if payment_date is None: self.payment_date = now else: self.payment_date = payment_date self.save( update_fields=['status', 'is_updated_on', 'highest_status', 'with_delivery_point', 'payment_date']) else: self.save(update_fields=['status', 'is_updated_on', 'highest_status', 'with_delivery_point']) menu_pool.clear() cache.clear() else: # /!\ If one delivery point has been closed, I may not close anymore by producer Purchase.objects.filter(permanence_id=self.id, producer__in=producers_id).order_by('?').update( status=new_status) ProducerInvoice.objects.filter(permanence_id=self.id, producer__in=producers_id).order_by( '?').update(status=new_status) def duplicate(self, dates): creation_counter = 0 short_name = self.safe_translation_getter( 'short_name', any_language=True, default=EMPTY_STRING ) cur_language = translation.get_language() for date in dates: delta_days = (date - self.permanence_date).days # Mandatory because of Parler if short_name != EMPTY_STRING: already_exists = Permanence.objects.filter( permanence_date=date, translations__language_code=cur_language, translations__short_name=short_name ).exists() else: already_exists = False for existing_permanence in Permanence.objects.filter( permanence_date=date ): try: short_name = existing_permanence.short_name already_exists = short_name == EMPTY_STRING except TranslationDoesNotExist: already_exists = True if already_exists: break if not already_exists: creation_counter += 1 new_permanence = Permanence.objects.create( permanence_date=date ) self.duplicate_short_name( new_permanence, cur_language=translation.get_language(), ) for permanence_board in PermanenceBoard.objects.filter( permanence=self ): PermanenceBoard.objects.create( permanence=new_permanence, permanence_role=permanence_board.permanence_role ) for delivery_board in DeliveryBoard.objects.filter( permanence=self ): if delivery_board.delivery_date is not None: new_delivery_board = DeliveryBoard.objects.create( permanence=new_permanence, delivery_point=delivery_board.delivery_point, delivery_date=delivery_board.delivery_date + datetime.timedelta(days=delta_days) ) else: new_delivery_board = DeliveryBoard.objects.create( permanence=new_permanence, delivery_point=delivery_board.delivery_point, ) for language in settings.PARLER_LANGUAGES[settings.SITE_ID]: language_code = language["code"] translation.activate(language_code) new_delivery_board.set_current_language(language_code) delivery_board.set_current_language(language_code) try: new_delivery_board.delivery_comment = delivery_board.delivery_comment new_delivery_board.save() except TranslationDoesNotExist: pass translation.activate(cur_language) for a_producer in self.producers.all(): new_permanence.producers.add(a_producer) return creation_counter def duplicate_short_name(self, new_permanence, cur_language): for language in settings.PARLER_LANGUAGES[settings.SITE_ID]: language_code = language["code"] translation.activate(language_code) new_permanence.set_current_language(language_code) self.set_current_language(language_code) try: new_permanence.short_name = self.short_name new_permanence.save() except TranslationDoesNotExist: pass translation.activate(cur_language) return new_permanence def create_child(self, status): child_permanence = Permanence.objects.create( permanence_date=self.permanence_date, master_permanence_id=self.id, status=status ) return self.duplicate_short_name( child_permanence, cur_language=translation.get_language(), ) def recalculate_order_amount(self, offer_item_qs=None, re_init=False, send_to_producer=False): from repanier.models.purchase import Purchase getcontext().rounding = ROUND_HALF_UP if send_to_producer or re_init: assert offer_item_qs is None, 'offer_item_qs must be set to None when send_to_producer or re_init' ProducerInvoice.objects.filter( permanence_id=self.id ).update( total_price_with_tax=DECIMAL_ZERO, total_vat=DECIMAL_ZERO, total_deposit=DECIMAL_ZERO, ) CustomerInvoice.objects.filter( permanence_id=self.id ).update( total_price_with_tax=DECIMAL_ZERO, total_vat=DECIMAL_ZERO, total_deposit=DECIMAL_ZERO ) CustomerProducerInvoice.objects.filter( permanence_id=self.id ).update( total_purchase_with_tax=DECIMAL_ZERO, total_selling_with_tax=DECIMAL_ZERO ) OfferItem.objects.filter( permanence_id=self.id ).update( quantity_invoiced=DECIMAL_ZERO, total_purchase_with_tax=DECIMAL_ZERO, total_selling_with_tax=DECIMAL_ZERO ) self.total_purchase_with_tax=DECIMAL_ZERO self.total_selling_with_tax=DECIMAL_ZERO self.total_purchase_vat=DECIMAL_ZERO self.total_selling_vat=DECIMAL_ZERO for offer_item in OfferItem.objects.filter( permanence_id=self.id, is_active=True, manage_replenishment=True ).exclude(add_2_stock=DECIMAL_ZERO).order_by('?'): # Recalculate the total_price_with_tax of ProducerInvoice and # the total_purchase_with_tax of OfferItem # taking into account "add_2_stock" offer_item.previous_add_2_stock = DECIMAL_ZERO offer_item.save() if offer_item_qs is not None: purchase_set = Purchase.objects \ .filter(permanence_id=self.id, offer_item__in=offer_item_qs) \ .order_by('?') else: purchase_set = Purchase.objects \ .filter(permanence_id=self.id) \ .order_by('?') for a_purchase in purchase_set.select_related("offer_item"): # Recalculate the total_price_with_tax of ProducerInvoice, # the total_price_with_tax of CustomerInvoice, # the total_purchase_with_tax + total_selling_with_tax of CustomerProducerInvoice, # and quantity_invoiced + total_purchase_with_tax + total_selling_with_tax of OfferItem if send_to_producer or re_init: a_purchase.previous_quantity_ordered = DECIMAL_ZERO a_purchase.previous_quantity_invoiced = DECIMAL_ZERO a_purchase.previous_purchase_price = DECIMAL_ZERO a_purchase.previous_selling_price = DECIMAL_ZERO a_purchase.previous_producer_vat = DECIMAL_ZERO a_purchase.previous_customer_vat = DECIMAL_ZERO a_purchase.previous_deposit = DECIMAL_ZERO if send_to_producer: offer_item = a_purchase.offer_item if offer_item.order_unit == PRODUCT_ORDER_UNIT_PC_KG: a_purchase.quantity_invoiced = (a_purchase.quantity_ordered * offer_item.order_average_weight) \ .quantize(FOUR_DECIMALS) else: a_purchase.quantity_invoiced = a_purchase.quantity_ordered a_purchase.save() self.save() def recalculate_profit(self): from repanier.models.purchase import Purchase getcontext().rounding = ROUND_HALF_UP result_set = CustomerInvoice.objects.filter( permanence_id=self.id, is_group=True, ).order_by('?').aggregate( Sum('delta_price_with_tax'), Sum('delta_vat'), Sum('delta_transport') ) if result_set["delta_price_with_tax__sum"] is not None: ci_sum_delta_price_with_tax = result_set["delta_price_with_tax__sum"] else: ci_sum_delta_price_with_tax = DECIMAL_ZERO if result_set["delta_vat__sum"] is not None: ci_sum_delta_vat = result_set["delta_vat__sum"] else: ci_sum_delta_vat = DECIMAL_ZERO if result_set["delta_transport__sum"] is not None: ci_sum_delta_transport = result_set["delta_transport__sum"] else: ci_sum_delta_transport = DECIMAL_ZERO result_set = Purchase.objects.filter( permanence_id=self.id, offer_item__price_list_multiplier__gte=DECIMAL_ONE ).order_by('?').aggregate( Sum('purchase_price'), Sum('selling_price'), Sum('producer_vat'), Sum('customer_vat'), ) if result_set["purchase_price__sum"] is not None: purchase_price = result_set["purchase_price__sum"] else: purchase_price = DECIMAL_ZERO if result_set["selling_price__sum"] is not None: selling_price = result_set["selling_price__sum"] else: selling_price = DECIMAL_ZERO selling_price += ci_sum_delta_price_with_tax + ci_sum_delta_transport if result_set["producer_vat__sum"] is not None: producer_vat = result_set["producer_vat__sum"] else: producer_vat = DECIMAL_ZERO if result_set["customer_vat__sum"] is not None: customer_vat = result_set["customer_vat__sum"] else: customer_vat = DECIMAL_ZERO customer_vat += ci_sum_delta_vat self.total_purchase_with_tax = purchase_price self.total_selling_with_tax = selling_price self.total_purchase_vat = producer_vat self.total_selling_vat = customer_vat result_set = Purchase.objects.filter( permanence_id=self.id, offer_item__price_list_multiplier__lt=DECIMAL_ONE ).order_by('?').aggregate( Sum('selling_price'), Sum('customer_vat'), ) if result_set["selling_price__sum"] is not None: selling_price = result_set["selling_price__sum"] else: selling_price = DECIMAL_ZERO if result_set["customer_vat__sum"] is not None: customer_vat = result_set["customer_vat__sum"] else: customer_vat = DECIMAL_ZERO self.total_purchase_with_tax += selling_price self.total_selling_with_tax += selling_price self.total_purchase_vat += customer_vat self.total_selling_vat += customer_vat @cached_property def get_new_products(self): assert self.status < PERMANENCE_SEND result = [] for a_producer in self.producers.all(): current_products = list(OfferItem.objects.filter( is_active=True, may_order=True, order_unit__lt=PRODUCT_ORDER_UNIT_DEPOSIT, # Don't display technical products. permanence_id=self.id, producer=a_producer ).values_list( 'product', flat=True ).order_by('?')) six_months_ago = timezone.now().date() - datetime.timedelta(days=6*30) previous_permanence = Permanence.objects.filter( status__gte=PERMANENCE_SEND, producers=a_producer, permanence_date__gte=six_months_ago ).order_by( "-permanence_date", "status" ).first() if previous_permanence is not None: previous_products = list(OfferItem.objects.filter( is_active=True, may_order=True, order_unit__lt=PRODUCT_ORDER_UNIT_DEPOSIT, # Don't display technical products. permanence_id=previous_permanence.id, producer=a_producer ).values_list( 'product', flat=True ).order_by('?')) new_products = [item for item in current_products if item not in previous_products] else: new_products = current_products qs = OfferItem.objects.filter( permanence_id=self.id, product__in=new_products, translations__language_code=translation.get_language() ).order_by( "translations__order_sort_order" ) for o in qs: result.append('<li>%s, %s, %s</li>' % ( o.get_long_name(with_box_unicode=False), o.producer.short_profile_name, o.email_offer_price_with_vat, )) if result: return mark_safe('<ul>%s</ul>' % "".join(result)) return EMPTY_STRING def get_full_status_display(self): need_to_refresh_status = self.status in [ PERMANENCE_WAIT_FOR_PRE_OPEN, PERMANENCE_WAIT_FOR_OPEN, PERMANENCE_WAIT_FOR_CLOSED, PERMANENCE_WAIT_FOR_SEND, PERMANENCE_WAIT_FOR_INVOICED ] if self.with_delivery_point: status_list = [] status = None status_counter = 0 for delivery in DeliveryBoard.objects.filter(permanence_id=self.id).order_by("status", "id"): need_to_refresh_status |= delivery.status in [ PERMANENCE_WAIT_FOR_PRE_OPEN, PERMANENCE_WAIT_FOR_OPEN, PERMANENCE_WAIT_FOR_CLOSED, PERMANENCE_WAIT_FOR_SEND, PERMANENCE_WAIT_FOR_INVOICED ] if status != delivery.status: status = delivery.status status_counter += 1 status_list.append("<b>%s</b>" % delivery.get_status_display()) status_list.append("- %s" % delivery.get_delivery_display(admin=True)) # if status_counter > 1: message = "<br/>".join(status_list) # else: # message = self.get_status_display() else: message = self.get_status_display() if need_to_refresh_status: url = urlresolvers.reverse( 'display_status', args=(self.id,) ) progress = "◤◥◢◣"[self.gauge] # "◴◷◶◵" "▛▜▟▙" self.gauge = (self.gauge + 1) % 4 self.save(update_fields=['gauge']) msg_html = """ <div class="wrap-text" id="id_get_status_%d"> <script type="text/javascript"> window.setTimeout(function(){ django.jQuery.ajax({ url: '%s', cache: false, async: false, success: function (result) { django.jQuery("#id_get_status_%d").html(result); } }); }, 500); </script> %s %s</div> """ % ( self.id, url, self.id, progress, message ) else: msg_html = '<div class="wrap-text">%s</div>' % message return mark_safe(msg_html) get_full_status_display.short_description = (_("Status")) get_full_status_display.allow_tags = True def get_permanence_display(self): short_name = self.safe_translation_getter( 'short_name', any_language=True ) if short_name: permanence_display = "%s" % short_name else: permanence_display = '%s%s' % ( repanier.apps.REPANIER_SETTINGS_PERMANENCE_ON_NAME, self.permanence_date.strftime(settings.DJANGO_SETTINGS_DATE) ) return permanence_display def get_permanence_admin_display(self): if self.status == PERMANENCE_INVOICED and self.total_selling_with_tax.amount != DECIMAL_ZERO: profit = self.total_selling_with_tax.amount - self.total_purchase_with_tax.amount # profit = self.total_selling_with_tax.amount - self.total_selling_vat.amount - self.total_purchase_with_tax.amount + self.total_purchase_vat.amount if profit != DECIMAL_ZERO: return '%s<br/>%s<br/>💶 %s' % ( self.get_permanence_display(), self.total_selling_with_tax, RepanierMoney(profit) ) return '%s<br/>%s' % ( self.get_permanence_display(), self.total_selling_with_tax) else: return self.get_permanence_display() get_permanence_admin_display.short_description = _("Offers") get_permanence_admin_display.allow_tags = True def get_permanence_customer_display(self, with_status=True): if with_status: if self.with_delivery_point: if self.status == PERMANENCE_OPENED: deliveries_count = 0 else: deliveries_qs = DeliveryBoard.objects.filter( permanence_id=self.id, status=PERMANENCE_OPENED ).order_by('?') deliveries_count = deliveries_qs.count() else: deliveries_count = 0 if deliveries_count == 0: if self.status != PERMANENCE_SEND: return "%s - %s" % (self.get_permanence_display(), self.get_status_display()) else: return "%s - %s" % (self.get_permanence_display(), _('orders closed')) return self.get_permanence_display() def __str__(self): return self.get_permanence_display() class Meta: verbose_name = _('order') verbose_name_plural = _('orders')
class Configuration(TranslatableModel): group_name = models.CharField(_("Name of the group"), max_length=50, default=EMPTY_STRING) login_attempt_counter = models.DecimalField(_("Login attempt counter"), default=DECIMAL_ZERO, max_digits=2, decimal_places=0) password_reset_on = models.DateTimeField(_("Password reset on"), null=True, blank=True, default=None) name = models.CharField(max_length=3, choices=LUT_PERMANENCE_NAME, default=PERMANENCE_NAME_PERMANENCE, verbose_name=_("Offers name")) currency = models.CharField(max_length=3, choices=LUT_CURRENCY, default=CURRENCY_EUR, verbose_name=_("Currency")) max_week_wo_participation = models.DecimalField( _("Alert the customer after this number of weeks without participation" ), help_text=_("0 mean : never display a pop up."), default=DECIMAL_ZERO, max_digits=2, decimal_places=0, validators=[MinValueValidator(0)]) send_abstract_order_mail_to_customer = models.BooleanField( _("Send abstract order mail to customers"), default=False) send_order_mail_to_board = models.BooleanField( _("Send an order distribution email to members registered for a task"), default=True) send_invoice_mail_to_customer = models.BooleanField( _("Send invoice mail to customers"), default=True) send_invoice_mail_to_producer = models.BooleanField( _("Send invoice mail to producers"), default=False) invoice = models.BooleanField(_("Enable accounting module"), default=True) display_anonymous_order_form = models.BooleanField( _("Allow the anonymous visitor to see the customer order screen"), default=True) display_who_is_who = models.BooleanField(_("Display the \"who's who\""), default=True) xlsx_portrait = models.BooleanField( _("Always generate XLSX files in portrait mode"), default=False) bank_account = models.CharField(_("Bank account"), max_length=100, null=True, blank=True, default=EMPTY_STRING) vat_id = models.CharField(_("VAT id"), max_length=20, null=True, blank=True, default=EMPTY_STRING) page_break_on_customer_check = models.BooleanField( _("Page break on customer check"), default=False) sms_gateway_mail = models.EmailField( _("Sms gateway email"), help_text= _("To actually send sms, use for e.g. on a GSM : https://play.google.com/store/apps/details?id=eu.apksoft.android.smsgateway" ), max_length=50, null=True, blank=True, default=EMPTY_STRING) membership_fee = ModelMoneyField(_("Membership fee"), default=DECIMAL_ZERO, max_digits=8, decimal_places=2) membership_fee_duration = models.DecimalField( _("Membership fee duration"), help_text=_("Number of month(s). 0 mean : no membership fee."), default=DECIMAL_ZERO, max_digits=3, decimal_places=0, validators=[MinValueValidator(0)]) home_site = models.URLField(_("Home site"), null=True, blank=True, default=EMPTY_STRING) permanence_of_last_cancelled_invoice = models.ForeignKey( 'Permanence', on_delete=models.PROTECT, blank=True, null=True) transport = ModelMoneyField( _("Shipping cost"), help_text=_("This amount is added to order less than min_transport."), default=DECIMAL_ZERO, max_digits=5, decimal_places=2, validators=[MinValueValidator(0)]) min_transport = ModelMoneyField( _("Minium order amount for free shipping cost"), help_text=_( "This is the minimum order amount to avoid shipping cost."), default=DECIMAL_ZERO, max_digits=5, decimal_places=2, validators=[MinValueValidator(0)]) email_is_custom = models.BooleanField(_("Email is customised"), default=False) email_host = models.CharField( _("Email host"), help_text= _("For @gmail.com, see: https://mail.google.com/mail/u/0/#settings/fwdandpop and activate POP" ), max_length=50, null=True, blank=True, default="smtp.gmail.com") email_port = models.IntegerField( _("Email port"), help_text=_("Usually 587 for @gmail.com, otherwise 25"), blank=True, null=True, default=587) email_use_tls = models.BooleanField( _("Email use tls"), help_text=_("TLS is used otherwise SSL is used"), default=True) email_host_user = models.EmailField(_("Email host user"), help_text=settings.DEFAULT_FROM_EMAIL, max_length=50, null=True, blank=True, default=settings.DEFAULT_FROM_EMAIL) email_host_password = models.CharField( _("Email host password"), help_text= _("For @gmail.com, you must generate an application password, see: https://security.google.com/settings/security/apppasswords" ), max_length=25, null=True, blank=True, default=EMPTY_STRING) db_version = models.PositiveSmallIntegerField(default=0) translations = TranslatedFields( group_label=models.CharField( _("Label to mention on the invoices of the group"), max_length=100, default=EMPTY_STRING, blank=True), how_to_register=HTMLField(_("How to register"), help_text=EMPTY_STRING, configuration='CKEDITOR_SETTINGS_MODEL2', default=EMPTY_STRING, blank=True), offer_customer_mail=HTMLField(_( "Contents of the order opening email sent to consumers authorized to order" ), help_text=EMPTY_STRING, configuration='CKEDITOR_SETTINGS_MODEL2', default=EMPTY_STRING, blank=True), offer_producer_mail=HTMLField(_("Email content"), help_text=EMPTY_STRING, configuration='CKEDITOR_SETTINGS_MODEL2', default=EMPTY_STRING, blank=True), order_customer_mail=HTMLField(_( "Content of the order confirmation email sent to the consumers concerned" ), help_text=EMPTY_STRING, configuration='CKEDITOR_SETTINGS_MODEL2', default=EMPTY_STRING, blank=True), cancel_order_customer_mail=HTMLField( _("Content of the email in case of cancellation of the order sent to the consumers concerned" ), help_text=EMPTY_STRING, configuration='CKEDITOR_SETTINGS_MODEL2', default=EMPTY_STRING, blank=True), order_staff_mail=HTMLField(_( "Content of the order distribution email sent to the members enrolled to a task" ), help_text=EMPTY_STRING, configuration='CKEDITOR_SETTINGS_MODEL2', default=EMPTY_STRING, blank=True), order_producer_mail=HTMLField(_( "Content of the order confirmation email sent to the producers concerned" ), help_text=EMPTY_STRING, configuration='CKEDITOR_SETTINGS_MODEL2', default=EMPTY_STRING, blank=True), invoice_customer_mail=HTMLField( _("Content of the invoice confirmation email sent to the customers concerned" ), help_text=EMPTY_STRING, configuration='CKEDITOR_SETTINGS_MODEL2', default=EMPTY_STRING, blank=True), invoice_producer_mail=HTMLField( _("Content of the payment confirmation email sent to the producers concerned" ), help_text=EMPTY_STRING, configuration='CKEDITOR_SETTINGS_MODEL2', default=EMPTY_STRING, blank=True), ) def clean(self): try: template = Template(self.offer_customer_mail) except Exception as error_str: raise ValidationError( mark_safe("{} : {}".format(self.offer_customer_mail, error_str))) try: template = Template(self.offer_producer_mail) except Exception as error_str: raise ValidationError( mark_safe("{} : {}".format(self.offer_producer_mail, error_str))) try: template = Template(self.order_customer_mail) except Exception as error_str: raise ValidationError( mark_safe("{} : {}".format(self.order_customer_mail, error_str))) try: template = Template(self.order_staff_mail) except Exception as error_str: raise ValidationError( mark_safe("{} : {}".format(self.order_staff_mail, error_str))) try: template = Template(self.order_producer_mail) except Exception as error_str: raise ValidationError( mark_safe("{} : {}".format(self.order_producer_mail, error_str))) if settings.REPANIER_SETTINGS_MANAGE_ACCOUNTING: try: template = Template(self.invoice_customer_mail) except Exception as error_str: raise ValidationError( mark_safe("{} : {}".format(self.invoice_customer_mail, error_str))) try: template = Template(self.invoice_producer_mail) except Exception as error_str: raise ValidationError( mark_safe("{} : {}".format(self.invoice_producer_mail, error_str))) @classmethod def init_repanier(cls): from repanier.const import DECIMAL_ONE, PERMANENCE_NAME_PERMANENCE, CURRENCY_EUR from repanier.models.producer import Producer from repanier.models.bankaccount import BankAccount from repanier.models.staff import Staff from repanier.models.customer import Customer # Create the configuration record managed via the admin UI config = Configuration.objects.filter(id=DECIMAL_ONE).first() if config is not None: return config group_name = settings.REPANIER_SETTINGS_GROUP_NAME site = Site.objects.get_current() if site is not None: site.name = group_name site.domain = group_name site.save() config = Configuration.objects.create( group_name=group_name, name=PERMANENCE_NAME_PERMANENCE, bank_account="BE99 9999 9999 9999", currency=CURRENCY_EUR) config.init_email() config.save() # Create firsts users Producer.get_or_create_group() customer_buyinggroup = Customer.get_or_create_group() very_first_customer = Customer.get_or_create_the_very_first_customer() BankAccount.open_account(customer_buyinggroup=customer_buyinggroup, very_first_customer=very_first_customer) coordinator = Staff.get_or_create_any_coordinator() Staff.get_or_create_order_responsible() Staff.get_or_create_invoice_responsible() # Create and publish first web page if not coordinator.is_webmaster: # This should not be the case... return from cms.models import StaticPlaceholder from cms.constants import X_FRAME_OPTIONS_DENY from cms import api page = api.create_page(title=_("Home"), soft_root=False, template=settings.CMS_TEMPLATE_HOME, language=settings.LANGUAGE_CODE, published=True, parent=None, xframe_options=X_FRAME_OPTIONS_DENY, in_navigation=True) try: # New in CMS 3.5 page.set_as_homepage() except: pass placeholder = page.placeholders.get(slot="home-hero") api.add_plugin(placeholder=placeholder, plugin_type='TextPlugin', language=settings.LANGUAGE_CODE, body=settings.CMS_TEMPLATE_HOME_HERO) placeholder = page.placeholders.get(slot="home-col-1") api.add_plugin(placeholder=placeholder, plugin_type='TextPlugin', language=settings.LANGUAGE_CODE, body=settings.CMS_TEMPLATE_HOME_COL_1) placeholder = page.placeholders.get(slot="home-col-2") api.add_plugin(placeholder=placeholder, plugin_type='TextPlugin', language=settings.LANGUAGE_CODE, body=settings.CMS_TEMPLATE_HOME_COL_2) placeholder = page.placeholders.get(slot="home-col-3") api.add_plugin(placeholder=placeholder, plugin_type='TextPlugin', language=settings.LANGUAGE_CODE, body=settings.CMS_TEMPLATE_HOME_COL_3) static_placeholder = StaticPlaceholder(code="footer", # site_id=1 ) static_placeholder.save() api.add_plugin(placeholder=static_placeholder.draft, plugin_type='TextPlugin', language=settings.LANGUAGE_CODE, body='hello world footer') static_placeholder.publish(request=None, language=settings.LANGUAGE_CODE, force=True) api.publish_page(page=page, user=coordinator.user, language=settings.LANGUAGE_CODE) return config def init_email(self): for language in settings.PARLER_LANGUAGES[settings.SITE_ID]: language_code = language["code"] self.set_current_language(language_code) try: self.offer_customer_mail = """ Bonjour,<br /> <br /> Les commandes de la {{ permanence_link }} sont maintenant ouvertes auprès de : {{ offer_producer }}.<br /> {% if offer_description %}<br />{{ offer_description }}<br /> {% endif %} {% if offer_recent_detail %}<br /> Nouveauté(s) :<br /> {{ offer_recent_detail }}{% endif %}<br /> <br /> {{ signature }} """ self.offer_producer_mail = """ Cher/Chère {{ long_profile_name }},<br> <br> {% if offer_description != "" %}Voici l'annonce consommateur :<br> {{ offer_description }}<br> <br> {% endif %} Veuillez vérifier votre <strong>{{ offer_link }}</strong>.<br> <br> {{ signature }} """ self.order_customer_mail = """ Bonjour {{ long_basket_name }},<br> <br> En pièce jointe vous trouverez le montant de votre panier {{ short_basket_name }} de la {{ permanence_link }}.<br> <br> {{ last_balance }}<br> {{ order_amount }}<br> {% if on_hold_movement %}{{ on_hold_movement }}<br> {% endif %} {% if payment_needed %}{{ payment_needed }}<br> {% endif %}<br> <br> {{ signature }} """ self.cancel_order_customer_mail = """ Bonjour {{ long_basket_name }},<br> <br> La commande ci-jointe de votre panier {{ short_basket_name }} de la {{ permanence_link }} <b>a été annulée</b> car vous ne l'avez pas confirmée.<br> <br> {{ signature }} """ self.order_staff_mail = """ Cher/Chère membre de l'équipe de préparation,<br> <br> En pièce jointe vous trouverez la liste de préparation pour la {{ permanence_link }}.<br> <br> L'équipe de préparation est composée de :<br> {{ board_composition_and_description }}<br> <br> {{ signature }} """ self.order_producer_mail = """ Cher/Chère {{ name }},<br> <br> {% if order_empty %}Le groupe ne vous a rien acheté pour la {{ permanence_link }}.{% else %}En pièce jointe, vous trouverez la commande du groupe pour la {{ permanence }}.{% if duplicate %}<br> <strong>ATTENTION </strong>: La commande est présente en deux exemplaires. Le premier exemplaire est classé par produit et le duplicata est classé par panier.{% else %}{% endif %}{% endif %}<br> <br> {{ signature }} """ self.invoice_customer_mail = """ Bonjour {{ name }},<br> <br> En cliquant sur ce lien vous trouverez votre facture pour la {{ permanence_link }}.{% if invoice_description %}<br> <br> {{ invoice_description }}{% endif %} <br> {{ order_amount }}<br> {{ last_balance_link }}<br> {% if payment_needed %}{{ payment_needed }}<br> {% endif %}<br> <br> {{ signature }} """ self.invoice_producer_mail = """ Cher/Chère {{ profile_name }},<br> <br> En cliquant sur ce lien vous trouverez le détail de notre paiement pour la {{ permanence_link }}.<br> <br> {{ signature }} """ self.save_translations() except TranslationDoesNotExist: pass def upgrade_db(self): if self.db_version == 0: from repanier.models import Product, OfferItemWoReceiver, BankAccount, Permanence, Staff # Staff.objects.rebuild() Product.objects.filter(is_box=True).order_by('?').update( limit_order_quantity_to_stock=True) OfferItemWoReceiver.objects.filter( permanence__status__gte=PERMANENCE_SEND, order_unit=PRODUCT_ORDER_UNIT_PC_KG).order_by('?').update( use_order_unit_converted=True) for bank_account in BankAccount.objects.filter( permanence__isnull=False, producer__isnull=True, customer__isnull=True).order_by('?').only( "id", "permanence_id"): Permanence.objects.filter( id=bank_account.permanence_id, invoice_sort_order__isnull=True).order_by('?').update( invoice_sort_order=bank_account.id) for permanence in Permanence.objects.filter( status__in=[PERMANENCE_CANCELLED, PERMANENCE_ARCHIVED], invoice_sort_order__isnull=True).order_by('?'): bank_account = BankAccount.get_closest_to( permanence.permanence_date) if bank_account is not None: permanence.invoice_sort_order = bank_account.id permanence.save(update_fields=['invoice_sort_order']) Staff.objects.order_by('?').update( is_order_manager=F('is_reply_to_order_email'), is_invoice_manager=F('is_reply_to_invoice_email'), is_order_referent=F('is_contributor')) self.db_version = 1 if self.db_version == 1: for user in User.objects.filter(is_staff=False).order_by('?'): user.first_name = EMPTY_STRING user.last_name = user.username[:30] user.save() for user in User.objects.filter(is_staff=True, is_superuser=False).order_by('?'): user.first_name = EMPTY_STRING user.last_name = user.email[:30] user.save() self.db_version = 2 if self.db_version == 2: from repanier.models import Staff Staff.objects.order_by('?').update( is_repanier_admin=F('is_coordinator'), ) Staff.objects.filter(is_repanier_admin=True).order_by('?').update( can_be_contacted=True, ) Staff.objects.filter(is_order_manager=True).order_by('?').update( can_be_contacted=True, ) Staff.objects.filter(is_invoice_manager=True).order_by('?').update( can_be_contacted=True, ) Staff.objects.filter( is_invoice_referent=True).order_by('?').update( is_invoice_manager=True, ) Staff.objects.filter(is_order_referent=True).order_by('?').update( is_order_manager=True, ) self.db_version = 3 def __str__(self): return self.group_name class Meta: verbose_name = _("Configuration") verbose_name_plural = _("Configurations")
class CustomerInvoice(Invoice): customer = models.ForeignKey('Customer', verbose_name=_("Customer"), on_delete=models.PROTECT) customer_charged = models.ForeignKey('Customer', verbose_name=_("Customer"), related_name='invoices_paid', blank=True, null=True, on_delete=models.PROTECT, db_index=True) delivery = models.ForeignKey('DeliveryBoard', verbose_name=_("Delivery board"), null=True, blank=True, default=None, on_delete=models.PROTECT) # IMPORTANT: default = True -> for the order form, to display nothing at the begin of the order # is_order_confirm_send and total_price_with_tax = 0 --> display nothing # otherwise display # - send a mail with the order to me # - confirm the order (if REPANIER_SETTINGS_CUSTOMER_MUST_CONFIRM_ORDER) and send a mail with the order to me # - mail send to XYZ # - order confirmed (if REPANIER_SETTINGS_CUSTOMER_MUST_CONFIRM_ORDER) and mail send to XYZ is_order_confirm_send = models.BooleanField( _("Confirmation of the order send"), choices=settings.LUT_CONFIRM, default=False) invoice_sort_order = models.IntegerField(_("Invoice sort order"), default=None, blank=True, null=True, db_index=True) price_list_multiplier = models.DecimalField( _("Delivery point coefficient applied to the producer tariff to calculate the consumer tariff" ), help_text= _("This multiplier is applied once for groups with entitled customer or at each customer invoice for open groups." ), default=DECIMAL_ONE, max_digits=5, decimal_places=4, blank=True, validators=[MinValueValidator(0)]) transport = ModelMoneyField( _("Delivery point shipping cost"), help_text= _("This amount is added once for groups with entitled customer or at each customer for open groups." ), default=DECIMAL_ZERO, max_digits=5, decimal_places=2, validators=[MinValueValidator(0)]) min_transport = ModelMoneyField( _("Minium order amount for free shipping cost"), help_text=_( "This is the minimum order amount to avoid shipping cost."), default=DECIMAL_ZERO, max_digits=5, decimal_places=2, validators=[MinValueValidator(0)]) master_permanence = models.ForeignKey( 'Permanence', verbose_name=_("Master permanence"), related_name='child_customer_invoice', blank=True, null=True, default=None, on_delete=models.PROTECT, db_index=True) is_group = models.BooleanField(_("Group"), default=False) def get_abs_delta_vat(self): return abs(self.delta_vat) def get_total_price_with_tax(self, customer_charged=False): if self.customer_id == self.customer_charged_id: return self.total_price_with_tax + self.delta_price_with_tax + self.delta_transport else: if self.status < PERMANENCE_INVOICED or not customer_charged: return self.total_price_with_tax else: return self.customer_charged # if self.total_price_with_tax != DECIMAL_ZERO else RepanierMoney() def get_total_price_wo_tax(self): return self.get_total_price_with_tax() - self.get_total_tax() def get_total_tax(self): # round to 2 decimals return RepanierMoney(self.total_vat.amount + self.delta_vat.amount) @property def has_purchase(self): if self.total_price_with_tax.amount != DECIMAL_ZERO or self.is_order_confirm_send: return True from repanier.models.purchase import PurchaseWoReceiver result = False result_set = PurchaseWoReceiver.objects.filter( permanence_id=self.permanence_id, customer_invoice_id=self.id).order_by('?').aggregate( Sum('quantity_ordered'), Sum('quantity_invoiced'), ) if result_set["quantity_ordered__sum"] is not None: sum_quantity_ordered = result_set["quantity_ordered__sum"] if sum_quantity_ordered != DECIMAL_ZERO: result = True if result_set["quantity_invoiced__sum"] is not None: sum_quantity_invoiced = result_set["quantity_invoiced__sum"] if sum_quantity_invoiced != DECIMAL_ZERO: result = True return result @transaction.atomic def set_delivery(self, delivery): # May not use delivery_id because it won't reload customer_invoice.delivery # Important # If it's an invoice of a member of a group : # self.customer_charged_id != self.customer_id # self.customer_charged_id == owner of the group # price_list_multiplier = DECIMAL_ONE # Else : # self.customer_charged_id = self.customer_id # price_list_multiplier may vary from repanier.apps import REPANIER_SETTINGS_TRANSPORT, REPANIER_SETTINGS_MIN_TRANSPORT if delivery is None: if self.permanence.with_delivery_point: # If the customer is member of a group set the group as default delivery point delivery_point = self.customer.delivery_point delivery = DeliveryBoard.objects.filter( delivery_point=delivery_point, permanence=self.permanence).order_by('?').first() else: delivery_point = None else: delivery_point = delivery.delivery_point self.delivery = delivery if delivery_point is None: self.customer_charged = self.customer self.price_list_multiplier = DECIMAL_ONE self.transport = REPANIER_SETTINGS_TRANSPORT self.min_transport = REPANIER_SETTINGS_MIN_TRANSPORT else: customer_responsible = delivery_point.customer_responsible if customer_responsible is None: self.customer_charged = self.customer self.price_list_multiplier = DECIMAL_ONE self.transport = delivery_point.transport self.min_transport = delivery_point.min_transport else: self.customer_charged = customer_responsible self.price_list_multiplier = DECIMAL_ONE self.transport = REPANIER_MONEY_ZERO self.min_transport = REPANIER_MONEY_ZERO if self.customer_id != customer_responsible.id: customer_invoice_charged = CustomerInvoice.objects.filter( permanence_id=self.permanence_id, customer_id=customer_responsible.id).order_by('?') if not customer_invoice_charged.exists(): CustomerInvoice.objects.create( permanence_id=self.permanence_id, customer_id=customer_responsible.id, status=self.status, customer_charged_id=customer_responsible.id, price_list_multiplier=customer_responsible. price_list_multiplier, transport=delivery_point.transport, min_transport=delivery_point.min_transport, is_order_confirm_send=True, is_group=True, delivery=delivery) def get_html_my_order_confirmation(self, permanence, is_basket=False, basket_message=EMPTY_STRING): if permanence.with_delivery_point: if self.delivery is not None: label = self.delivery.get_delivery_customer_display() delivery_id = self.delivery_id else: delivery_id = 0 if self.customer.delivery_point is not None: qs = DeliveryBoard.objects.filter( Q(permanence_id=permanence.id, delivery_point_id=self.customer.delivery_point_id, status=PERMANENCE_OPENED) | Q(permanence_id=permanence.id, delivery_point__customer_responsible__isnull=True, status=PERMANENCE_OPENED)).order_by('?') else: qs = DeliveryBoard.objects.filter( permanence_id=permanence.id, delivery_point__customer_responsible__isnull=True, status=PERMANENCE_OPENED).order_by('?') if qs.exists(): label = "{}".format(_('Please, select a delivery point')) CustomerInvoice.objects.filter( permanence_id=permanence.id, customer_id=self.customer_id).order_by('?').update( status=PERMANENCE_OPENED) else: label = "{}".format( _('No delivery point is open for you. You can not place order.' )) # IMPORTANT : # 1 / This prohibit to place an order into the customer UI # 2 / task_order.close_send_order will delete any CLOSED orders without any delivery point CustomerInvoice.objects.filter( permanence_id=permanence.id, customer_id=self.customer_id).order_by('?').update( status=PERMANENCE_CLOSED) if self.customer_id != self.customer_charged_id: msg_price = msg_transport = EMPTY_STRING else: if self.transport.amount <= DECIMAL_ZERO: transport = False msg_transport = EMPTY_STRING else: transport = True if self.min_transport.amount > DECIMAL_ZERO: msg_transport = "{}<br>".format( _('The shipping costs for this delivery point amount to %(transport)s for orders of less than %(min_transport)s.' ) % { 'transport': self.transport, 'min_transport': self.min_transport }) else: msg_transport = "{}<br>".format( _('The shipping costs for this delivery point amount to %(transport)s.' ) % { 'transport': self.transport, }) if self.price_list_multiplier == DECIMAL_ONE: msg_price = EMPTY_STRING else: if transport: if self.price_list_multiplier > DECIMAL_ONE: msg_price = "{}<br>".format( _('In addition, a surcharge of %(increase)s %% is applied to the billed total. It does not apply to deposits or fees.' ) % { 'increase': number_format( (self.price_list_multiplier - DECIMAL_ONE) * 100, 2) }) else: msg_price = "{}<br>".format( _('In addition a reduction of %(decrease)s %% is applied to the billed total. It does not apply to deposits or fees.' ) % { 'decrease': number_format( (DECIMAL_ONE - self.price_list_multiplier) * 100, 2) }) else: if self.price_list_multiplier > DECIMAL_ONE: msg_price = "{}<br>".format( _('For this delivery point, an overload of %(increase)s %% is applied to the billed total (out of deposit).' ) % { 'increase': number_format( (self.price_list_multiplier - DECIMAL_ONE) * 100, 2) }) else: msg_price = "{}<br>".format( _('For this delivery point, a reduction of %(decrease)s %% is applied to the invoiced total (out of deposit).' ) % { 'decrease': number_format( (DECIMAL_ONE - self.price_list_multiplier) * 100, 2) }) msg_delivery = """ {}<b><i> <select name=\"delivery\" id=\"delivery\" onmouseover=\"show_select_delivery_list_ajax({})\" onchange=\"delivery_ajax()\" class=\"form-control\"> <option value=\"{}\" selected>{}</option> </select> </i></b><br>{}{} """.format(_("Delivery point"), delivery_id, delivery_id, label, msg_transport, msg_price) else: msg_delivery = EMPTY_STRING msg_confirmation1 = EMPTY_STRING if not is_basket and not settings.REPANIER_SETTINGS_CUSTOMER_MUST_CONFIRM_ORDER: # or customer_invoice.total_price_with_tax.amount != DECIMAL_ZERO: # If REPANIER_SETTINGS_CUSTOMER_MUST_CONFIRM_ORDER, # then permanence.with_delivery_point is also True msg_html = EMPTY_STRING else: if self.is_order_confirm_send: msg_confirmation2 = self.customer.my_order_confirmation_email_send_to( ) msg_html = """ <div class="row"> <div class="panel panel-default"> <div class="panel-heading"> {} <p><font color="#51a351">{}</font><p/> {} </div> </div> </div> """.format(msg_delivery, msg_confirmation2, basket_message) else: msg_html = None btn_disabled = EMPTY_STRING if permanence.status == PERMANENCE_OPENED else "disabled" msg_confirmation2 = EMPTY_STRING if settings.REPANIER_SETTINGS_CUSTOMER_MUST_CONFIRM_ORDER: if is_basket: if self.status == PERMANENCE_OPENED: if (permanence.with_delivery_point and self.delivery is None) \ or not self.has_purchase: btn_disabled = "disabled" msg_confirmation1 = "<span style=\"color: red; \">{}</span><br>".format( _("⚠ Unconfirmed orders will be canceled.")) msg_confirmation2 = "<span class=\"glyphicon glyphicon-floppy-disk\"></span> {}".format( _("Confirm this order and receive an email containing its summary." )) else: href = reverse('order_view', args=(permanence.id, )) if self.status == PERMANENCE_OPENED: msg_confirmation1 = "<span style=\"color: red; \">{}</span><br>".format( _("⚠ Unconfirmed orders will be canceled.")) msg_confirmation2 = _( "➜ Go to the confirmation step of my order.") msg_html = """ <div class="row"> <div class="panel panel-default"> <div class="panel-heading"> {} {} <a href="{}?is_basket=yes" class="btn btn-info" {}>{}</a> </div> </div> </div> """.format(msg_delivery, msg_confirmation1, href, btn_disabled, msg_confirmation2) else: if is_basket: msg_confirmation2 = _( "Receive an email containing this order summary.") elif permanence.with_delivery_point: msg_html = """ <div class="row"> <div class="panel panel-default"> <div class="panel-heading"> {} </div> </div> </div> """.format(msg_delivery) else: msg_html = EMPTY_STRING if msg_html is None: if msg_confirmation2 == EMPTY_STRING: msg_html = """ <div class="row"> <div class="panel panel-default"> <div class="panel-heading"> {} <div class="clearfix"></div> {} </div> </div> </div> """.format(msg_delivery, basket_message) else: msg_html = """ <div class="row"> <div class="panel panel-default"> <div class="panel-heading"> {} {} <button id="btn_confirm_order" class="btn btn-info" {} onclick="btn_receive_order_email();">{}</button> <div class="clearfix"></div> {} </div> </div> </div> """.format(msg_delivery, msg_confirmation1, btn_disabled, msg_confirmation2, basket_message) return {"#span_btn_confirm_order": mark_safe(msg_html)} @transaction.atomic def confirm_order(self): from repanier.models.purchase import Purchase Purchase.objects.filter(customer_invoice__id=self.id).update( quantity_confirmed=F('quantity_ordered')) self.calculate_and_save_delta_buyinggroup() self.is_order_confirm_send = True def calculate_and_save_delta_buyinggroup(self): previous_delta_price_with_tax = self.delta_price_with_tax.amount previous_delta_vat = self.delta_vat.amount previous_delta_transport = self.delta_transport.amount self.calculate_delta_price() self.calculate_delta_transport() if previous_delta_price_with_tax != self.delta_price_with_tax.amount or previous_delta_vat != self.delta_vat.amount or previous_delta_transport != self.delta_transport.amount: producer_invoice_buyinggroup = ProducerInvoice.objects.filter( producer__represent_this_buyinggroup=True, permanence_id=self.permanence_id, ).order_by('?').first() if producer_invoice_buyinggroup is None: from repanier.models.producer import Producer producer_invoice_buyinggroup = ProducerInvoice.objects.create( producer=Producer.get_or_create_group(), permanence_id=self.permanence_id, status=self.permanence.status) producer_invoice_buyinggroup.delta_price_with_tax.amount += self.delta_price_with_tax.amount - previous_delta_price_with_tax producer_invoice_buyinggroup.delta_vat.amount += self.delta_vat.amount - previous_delta_vat producer_invoice_buyinggroup.delta_transport.amount += self.delta_transport.amount - previous_delta_transport producer_invoice_buyinggroup.save() def calculate_delta_price(self): from repanier.models.purchase import Purchase self.delta_price_with_tax.amount = DECIMAL_ZERO self.delta_vat.amount = DECIMAL_ZERO if self.customer_id == self.customer_charged_id: # It's an invoice of a group, or of a customer who is not member of a group : # self.customer_charged_id = self.customer_id # self.price_list_multiplier may vary if self.price_list_multiplier != DECIMAL_ONE: result_set = Purchase.objects.filter( permanence_id=self.permanence_id, customer_invoice__customer_charged_id=self.customer_id, is_resale_price_fixed=False).order_by('?').aggregate( Sum('customer_vat'), Sum('deposit'), Sum('selling_price')) if result_set["customer_vat__sum"] is not None: total_vat = result_set["customer_vat__sum"] else: total_vat = DECIMAL_ZERO if result_set["deposit__sum"] is not None: total_deposit = result_set["deposit__sum"] else: total_deposit = DECIMAL_ZERO if result_set["selling_price__sum"] is not None: total_price_with_tax = result_set["selling_price__sum"] else: total_price_with_tax = DECIMAL_ZERO total_price_with_tax_wo_deposit = total_price_with_tax - total_deposit self.delta_price_with_tax.amount = ( (total_price_with_tax_wo_deposit * self.price_list_multiplier).quantize(TWO_DECIMALS) - total_price_with_tax_wo_deposit) self.delta_vat.amount = -( (total_vat * self.price_list_multiplier ).quantize(FOUR_DECIMALS) - total_vat) result_set = Purchase.objects.filter( permanence_id=self.permanence_id, customer_invoice__customer_charged_id=self.customer_id, ).order_by('?').aggregate(Sum('customer_vat'), Sum('deposit'), Sum('selling_price')) else: # It's an invoice of a member of a group # self.customer_charged_id != self.customer_id # self.customer_charged_id == owner of the group # assertion : self.price_list_multiplier always == DECIMAL_ONE result_set = Purchase.objects.filter( permanence_id=self.permanence_id, customer_id=self.customer_id, ).order_by('?').aggregate(Sum('customer_vat'), Sum('deposit'), Sum('selling_price')) if result_set["customer_vat__sum"] is not None: self.total_vat.amount = result_set["customer_vat__sum"] else: self.total_vat.amount = DECIMAL_ZERO if result_set["deposit__sum"] is not None: self.total_deposit.amount = result_set["deposit__sum"] else: self.total_deposit.amount = DECIMAL_ZERO if result_set["selling_price__sum"] is not None: self.total_price_with_tax.amount = result_set["selling_price__sum"] else: self.total_price_with_tax.amount = DECIMAL_ZERO if settings.REPANIER_SETTINGS_ROUND_INVOICES: total_price = self.total_price_with_tax.amount + self.delta_price_with_tax.amount total_price_gov_be = round_gov_be(total_price) self.delta_price_with_tax.amount += (total_price_gov_be - total_price) def calculate_delta_transport(self): self.delta_transport.amount = DECIMAL_ZERO if self.master_permanence_id is None and self.transport.amount != DECIMAL_ZERO: # Calculate transport only on master customer invoice # But take into account the children customer invoices result_set = CustomerInvoice.objects.filter( master_permanence_id=self.permanence_id).order_by( '?').aggregate(Sum('total_price_with_tax'), Sum('delta_price_with_tax')) if result_set["total_price_with_tax__sum"] is not None: sum_total_price_with_tax = result_set[ "total_price_with_tax__sum"] else: sum_total_price_with_tax = DECIMAL_ZERO if result_set["delta_price_with_tax__sum"] is not None: sum_delta_price_with_tax = result_set[ "delta_price_with_tax__sum"] else: sum_delta_price_with_tax = DECIMAL_ZERO sum_total_price_with_tax += self.total_price_with_tax.amount sum_delta_price_with_tax += self.delta_price_with_tax.amount total_price_with_tax = sum_total_price_with_tax + sum_delta_price_with_tax if total_price_with_tax != DECIMAL_ZERO: if self.min_transport.amount == DECIMAL_ZERO: self.delta_transport.amount = self.transport.amount elif total_price_with_tax < self.min_transport.amount: self.delta_transport.amount = min( self.min_transport.amount - total_price_with_tax, self.transport.amount) def cancel_confirm_order(self): if self.is_order_confirm_send: # Change of confirmation status self.is_order_confirm_send = False return True else: # No change of confirmation status return False def create_child(self, new_permanence): if self.customer_id != self.customer_charged_id: # TODO : Créer la customer invoice du groupe customer_invoice = CustomerInvoice.objects.filter( permanence_id=self.permanence_id, customer_id=self.customer_charged_id).only("id").order_by('?') if not customer_invoice.exists(): customer_invoice = CustomerInvoice.objects.create( permanence_id=self.permanence_id, customer_id=self.customer_charged_id, customer_charged_id=self.customer_charged_id, status=self.status) customer_invoice.set_delivery(delivery=None) customer_invoice.save() return CustomerInvoice.objects.create( permanence_id=new_permanence.id, customer_id=self.customer_id, master_permanence_id=self.permanence_id, customer_charged_id=self.customer_charged_id, status=self.status) def cancel_if_unconfirmed(self, permanence): if settings.REPANIER_SETTINGS_CUSTOMER_MUST_CONFIRM_ORDER \ and not self.is_order_confirm_send \ and self.has_purchase: from repanier.email.email_order import export_order_2_1_customer from repanier.models.purchase import Purchase filename = "{0}-{1}.xlsx".format(_("Canceled order"), permanence) export_order_2_1_customer(self.customer, filename, permanence, cancel_order=True) purchase_qs = Purchase.objects.filter( customer_invoice_id=self.id, is_box_content=False, ).order_by('?') for a_purchase in purchase_qs.select_related("customer"): create_or_update_one_cart_item( customer=a_purchase.customer, offer_item_id=a_purchase.offer_item_id, q_order=DECIMAL_ZERO, batch_job=True, comment=_("Qty not confirmed : {}").format( number_format(a_purchase.quantity_ordered, 4))) def __str__(self): return "{}, {}".format(self.customer, self.permanence) class Meta: verbose_name = _("Customer invoice") verbose_name_plural = _("Customers invoices") unique_together = ( "permanence", "customer", )
class Item(TranslatableModel): producer = models.ForeignKey('Producer', verbose_name=_("Producer"), on_delete=models.PROTECT) department_for_customer = models.ForeignKey('LUT_DepartmentForCustomer', verbose_name=_("Department"), blank=True, null=True, on_delete=models.PROTECT) picture2 = AjaxPictureField(verbose_name=_("Picture"), null=True, blank=True, upload_to="product", size=SIZE_L) reference = models.CharField(_("Reference"), max_length=36, blank=True, null=True) order_unit = models.CharField( max_length=3, choices=LUT_PRODUCT_ORDER_UNIT, default=PRODUCT_ORDER_UNIT_PC, verbose_name=_("Order unit"), ) order_average_weight = models.DecimalField( _("Average weight / capacity"), default=DECIMAL_ZERO, max_digits=6, decimal_places=3, validators=[MinValueValidator(0)]) producer_unit_price = ModelMoneyField(_("Producer unit price"), default=DECIMAL_ZERO, max_digits=8, decimal_places=2) customer_unit_price = ModelMoneyField(_("Customer unit price"), default=DECIMAL_ZERO, max_digits=8, decimal_places=2) producer_vat = ModelMoneyField(_("VAT"), default=DECIMAL_ZERO, max_digits=8, decimal_places=4) customer_vat = ModelMoneyField(_("VAT"), default=DECIMAL_ZERO, max_digits=8, decimal_places=4) unit_deposit = ModelMoneyField( _("Deposit"), help_text=_('Deposit to add to the original unit price'), default=DECIMAL_ZERO, max_digits=8, decimal_places=2, validators=[MinValueValidator(0)]) vat_level = models.CharField( max_length=3, choices=LUT_ALL_VAT, # settings.LUT_VAT, default=settings.DICT_VAT_DEFAULT, verbose_name=_("Tax level")) wrapped = models.BooleanField(_('Individually wrapped by the producer'), default=False) customer_minimum_order_quantity = models.DecimalField( _("Minimum order quantity"), default=DECIMAL_ONE, max_digits=6, decimal_places=3, validators=[MinValueValidator(0)]) customer_increment_order_quantity = models.DecimalField( _("Then quantity of"), default=DECIMAL_ONE, max_digits=6, decimal_places=3, validators=[MinValueValidator(0)]) customer_alert_order_quantity = models.DecimalField( _("Alert quantity"), default=LIMIT_ORDER_QTY_ITEM, max_digits=6, decimal_places=3, validators=[MinValueValidator(0)]) producer_order_by_quantity = models.DecimalField( _("Producer order by quantity"), default=DECIMAL_ZERO, max_digits=6, decimal_places=3, validators=[MinValueValidator(0)]) placement = models.CharField( max_length=3, choices=LUT_PRODUCT_PLACEMENT, default=PRODUCT_PLACEMENT_BASKET, verbose_name=_("Product placement"), ) stock = models.DecimalField(_("Inventory"), default=DECIMAL_MAX_STOCK, max_digits=9, decimal_places=3, validators=[MinValueValidator(0)]) limit_order_quantity_to_stock = models.BooleanField( _("Limit maximum order qty of the group to stock qty"), default=False) is_box = models.BooleanField(default=False) # is_membership_fee = models.BooleanField(_("is_membership_fee"), default=False) # may_order = models.BooleanField(_("may_order"), default=True) is_active = models.BooleanField(_("Active"), default=True) @property def email_offer_price_with_vat(self): offer_price = self.get_reference_price() if offer_price == EMPTY_STRING: offer_price = self.get_unit_price() return offer_price def set_from(self, source): self.is_active = source.is_active self.picture2 = source.picture2 self.reference = source.reference self.department_for_customer_id = source.department_for_customer_id self.producer_id = source.producer_id self.order_unit = source.order_unit self.wrapped = source.wrapped self.order_average_weight = source.order_average_weight self.placement = source.placement self.vat_level = source.vat_level self.customer_unit_price = source.customer_unit_price self.customer_vat = source.customer_vat self.producer_unit_price = source.producer_unit_price self.producer_vat = source.producer_vat self.unit_deposit = source.unit_deposit self.limit_order_quantity_to_stock = source.limit_order_quantity_to_stock self.stock = source.stock self.customer_minimum_order_quantity = source.customer_minimum_order_quantity self.customer_increment_order_quantity = source.customer_increment_order_quantity self.customer_alert_order_quantity = source.customer_alert_order_quantity self.producer_order_by_quantity = source.producer_order_by_quantity self.is_box = source.is_box def recalculate_prices(self, producer_price_are_wo_vat, is_resale_price_fixed, price_list_multiplier): vat = DICT_VAT[self.vat_level] vat_rate = vat[DICT_VAT_RATE] if producer_price_are_wo_vat: self.producer_vat.amount = (self.producer_unit_price.amount * vat_rate).quantize(FOUR_DECIMALS) if not is_resale_price_fixed: if self.order_unit < PRODUCT_ORDER_UNIT_DEPOSIT: self.customer_unit_price.amount = ( self.producer_unit_price.amount * price_list_multiplier).quantize(TWO_DECIMALS) else: self.customer_unit_price = self.producer_unit_price self.customer_vat.amount = (self.customer_unit_price.amount * vat_rate).quantize(FOUR_DECIMALS) if not is_resale_price_fixed: self.customer_unit_price += self.customer_vat else: self.producer_vat.amount = self.producer_unit_price.amount - ( self.producer_unit_price.amount / (DECIMAL_ONE + vat_rate)).quantize(FOUR_DECIMALS) if not is_resale_price_fixed: if self.order_unit < PRODUCT_ORDER_UNIT_DEPOSIT: self.customer_unit_price.amount = ( self.producer_unit_price.amount * price_list_multiplier).quantize(TWO_DECIMALS) else: self.customer_unit_price = self.producer_unit_price self.customer_vat.amount = self.customer_unit_price.amount - ( self.customer_unit_price.amount / (DECIMAL_ONE + vat_rate)).quantize(FOUR_DECIMALS) def get_unit_price(self, customer_price=True): if customer_price: unit_price = self.customer_unit_price else: unit_price = self.producer_unit_price if self.order_unit in [ PRODUCT_ORDER_UNIT_KG, PRODUCT_ORDER_UNIT_PC_KG ]: return "{} {}".format(unit_price, _("/ kg")) elif self.order_unit == PRODUCT_ORDER_UNIT_LT: return "{} {}".format(unit_price, _("/ l")) elif self.order_unit not in [ PRODUCT_ORDER_UNIT_PC_PRICE_KG, PRODUCT_ORDER_UNIT_PC_PRICE_LT, PRODUCT_ORDER_UNIT_PC_PRICE_PC ]: return "{} {}".format(unit_price, _("/ piece")) else: return "{}".format(unit_price) def get_reference_price(self, customer_price=True): if self.order_average_weight > DECIMAL_ZERO and self.order_average_weight != DECIMAL_ONE: if self.order_unit in [ PRODUCT_ORDER_UNIT_PC_PRICE_KG, PRODUCT_ORDER_UNIT_PC_PRICE_LT, PRODUCT_ORDER_UNIT_PC_PRICE_PC ]: if customer_price: reference_price = self.customer_unit_price.amount / self.order_average_weight else: reference_price = self.producer_unit_price.amount / self.order_average_weight reference_price = RepanierMoney( reference_price.quantize(TWO_DECIMALS), 2) if self.order_unit == PRODUCT_ORDER_UNIT_PC_PRICE_KG: reference_unit = _("/ kg") elif self.order_unit == PRODUCT_ORDER_UNIT_PC_PRICE_LT: reference_unit = _("/ l") else: reference_unit = _("/ pc") return "{} {}".format(reference_price, reference_unit) else: return EMPTY_STRING else: return EMPTY_STRING def get_display(self, qty=0, order_unit=PRODUCT_ORDER_UNIT_PC, unit_price_amount=None, for_customer=True, for_order_select=False, without_price_display=False): magnitude = None display_qty = True if order_unit == PRODUCT_ORDER_UNIT_KG: if qty == DECIMAL_ZERO: unit = EMPTY_STRING elif for_customer and qty < 1: unit = "{}".format(_('gr')) magnitude = 1000 else: unit = "{}".format(_('kg')) elif order_unit == PRODUCT_ORDER_UNIT_LT: if qty == DECIMAL_ZERO: unit = EMPTY_STRING elif for_customer and qty < 1: unit = "{}".format(_('cl')) magnitude = 100 else: unit = "{}".format(_('l')) elif order_unit in [ PRODUCT_ORDER_UNIT_PC_KG, PRODUCT_ORDER_UNIT_PC_PRICE_KG ]: # display_qty = not (order_average_weight == 1 and order_unit == PRODUCT_ORDER_UNIT_PC_PRICE_KG) average_weight = self.order_average_weight if for_customer: average_weight *= qty if order_unit == PRODUCT_ORDER_UNIT_PC_KG and unit_price_amount is not None: unit_price_amount *= self.order_average_weight if average_weight < 1: average_weight_unit = _('gr') average_weight *= 1000 else: average_weight_unit = _('kg') decimal = 3 if average_weight == int(average_weight): decimal = 0 elif average_weight * 10 == int(average_weight * 10): decimal = 1 elif average_weight * 100 == int(average_weight * 100): decimal = 2 tilde = EMPTY_STRING if order_unit == PRODUCT_ORDER_UNIT_PC_KG: tilde = '~' if for_customer: if qty == DECIMAL_ZERO: unit = EMPTY_STRING else: if self.order_average_weight == 1 and order_unit == PRODUCT_ORDER_UNIT_PC_PRICE_KG: unit = "{}{} {}".format( tilde, number_format(average_weight, decimal), average_weight_unit) else: unit = "{}{}{}".format( tilde, number_format(average_weight, decimal), average_weight_unit) else: if qty == DECIMAL_ZERO: unit = EMPTY_STRING else: unit = "{}{}{}".format( tilde, number_format(average_weight, decimal), average_weight_unit) elif order_unit == PRODUCT_ORDER_UNIT_PC_PRICE_LT: display_qty = self.order_average_weight != 1 average_weight = self.order_average_weight if for_customer: average_weight *= qty if average_weight < 1: average_weight_unit = _('cl') average_weight *= 100 else: average_weight_unit = _('l') decimal = 3 if average_weight == int(average_weight): decimal = 0 elif average_weight * 10 == int(average_weight * 10): decimal = 1 elif average_weight * 100 == int(average_weight * 100): decimal = 2 if for_customer: if qty == DECIMAL_ZERO: unit = EMPTY_STRING else: if display_qty: unit = "{}{}".format( number_format(average_weight, decimal), average_weight_unit) else: unit = "{} {}".format( number_format(average_weight, decimal), average_weight_unit) else: if qty == DECIMAL_ZERO: unit = EMPTY_STRING else: unit = "{}{}".format( number_format(average_weight, decimal), average_weight_unit) elif order_unit == PRODUCT_ORDER_UNIT_PC_PRICE_PC: display_qty = self.order_average_weight != 1 average_weight = self.order_average_weight if for_customer: average_weight *= qty if qty == DECIMAL_ZERO: unit = EMPTY_STRING else: if average_weight < 2: pc_pcs = _('pc') else: pc_pcs = _('pcs') if display_qty: unit = "{}{}".format(number_format(average_weight, 0), pc_pcs) else: unit = "{} {}".format(number_format(average_weight, 0), pc_pcs) else: if average_weight == DECIMAL_ZERO: unit = EMPTY_STRING elif average_weight < 2: unit = "{} {}".format(number_format(average_weight, 0), _('pc')) else: unit = "{} {}".format(number_format(average_weight, 0), _('pcs')) else: if for_order_select: if qty == DECIMAL_ZERO: unit = EMPTY_STRING elif qty < 2: unit = "{}".format(_('unit')) else: unit = "{}".format(_('units')) else: unit = EMPTY_STRING if unit_price_amount is not None: price_display = " = {}".format( RepanierMoney(unit_price_amount * qty)) else: price_display = EMPTY_STRING if magnitude is not None: qty *= magnitude decimal = 3 if qty == int(qty): decimal = 0 elif qty * 10 == int(qty * 10): decimal = 1 elif qty * 100 == int(qty * 100): decimal = 2 if for_customer or for_order_select: if unit: if display_qty: qty_display = "{} ({})".format(number_format(qty, decimal), unit) else: qty_display = "{}".format(unit) else: qty_display = "{}".format(number_format(qty, decimal)) else: if unit: qty_display = "({})".format(unit) else: qty_display = EMPTY_STRING if without_price_display: return qty_display else: display = "{}{}".format(qty_display, price_display) return display def get_customer_alert_order_quantity(self): if settings.REPANIER_SETTINGS_STOCK and self.limit_order_quantity_to_stock: return "{}".format(_("Inventory")) return self.customer_alert_order_quantity get_customer_alert_order_quantity.short_description = (_("Alert quantity")) def get_long_name_with_producer_price(self): return self.get_long_name(customer_price=False) get_long_name_with_producer_price.short_description = (_("Long name")) get_long_name_with_producer_price.admin_order_field = 'translations__long_name' def get_qty_display(self): raise NotImplementedError def get_qty_and_price_display(self, customer_price=True): qty_display = self.get_qty_display() unit_price = self.get_unit_price(customer_price=customer_price) if len(qty_display) > 0: if self.unit_deposit.amount > DECIMAL_ZERO: return "{}; {} + ♻ {}".format(qty_display, unit_price, self.unit_deposit) else: return "{}; {}".format(qty_display, unit_price) else: if self.unit_deposit.amount > DECIMAL_ZERO: return "{} + ♻ {}".format(unit_price, self.unit_deposit) else: return "{}".format(unit_price) def get_long_name(self, customer_price=True): qty_and_price_display = self.get_qty_and_price_display(customer_price) if qty_and_price_display: result = "{} {}".format( self.safe_translation_getter('long_name', any_language=True), qty_and_price_display) else: result = "{}".format( self.safe_translation_getter('long_name', any_language=True)) return result get_long_name.short_description = (_("Long name")) get_long_name.admin_order_field = 'translations__long_name' def get_long_name_with_producer(self): if self.id is not None: return "{}, {}".format(self.producer.short_profile_name, self.get_long_name()) else: # Nedeed for django import export since django_import_export-0.4.5 return 'N/A' get_long_name_with_producer.short_description = (_("Long name")) get_long_name_with_producer.allow_tags = False get_long_name_with_producer.admin_order_field = 'translations__long_name' def __str__(self): return EMPTY_STRING class Meta: abstract = True
class BankAccount(models.Model): permanence = models.ForeignKey( "Permanence", verbose_name=REPANIER_SETTINGS_PERMANENCE_NAME, on_delete=models.PROTECT, blank=True, null=True, ) producer = models.ForeignKey( "Producer", verbose_name=_("Producer"), on_delete=models.PROTECT, blank=True, null=True, ) customer = models.ForeignKey( "Customer", verbose_name=_("Customer"), on_delete=models.PROTECT, blank=True, null=True, ) operation_date = models.DateField(_("Operation date"), db_index=True) operation_comment = models.CharField( _("Operation comment"), max_length=100, blank=True, default=EMPTY_STRING ) operation_status = models.CharField( max_length=3, choices=LUT_BANK_TOTAL, default=BANK_NOT_LATEST_TOTAL, verbose_name=_("Account balance status"), db_index=True, ) bank_amount_in = ModelMoneyField( _("Cash in"), help_text=_("Payment on the account"), max_digits=8, decimal_places=2, default=DECIMAL_ZERO, validators=[MinValueValidator(0)], ) bank_amount_out = ModelMoneyField( _("Cash out"), help_text=_("Payment from the account"), max_digits=8, decimal_places=2, default=DECIMAL_ZERO, validators=[MinValueValidator(0)], ) producer_invoice = models.ForeignKey( "ProducerInvoice", verbose_name=_("Producer invoice"), blank=True, null=True, on_delete=models.PROTECT, db_index=True, ) customer_invoice = models.ForeignKey( "CustomerInvoice", verbose_name=_("Customer invoice"), blank=True, null=True, on_delete=models.PROTECT, db_index=True, ) is_updated_on = models.DateTimeField(_("Updated on"), auto_now=True) @classmethod def open_account(cls, customer_buyinggroup, very_first_customer): bank_account = BankAccount.objects.filter().order_by("?") if not bank_account.exists(): BankAccount.objects.create( operation_status=BANK_LATEST_TOTAL, operation_date=timezone.now().date(), operation_comment=_("Account opening"), ) # Create this also prevent the deletion of the customer representing the buying group BankAccount.objects.create( operation_date=timezone.now().date(), customer=customer_buyinggroup, operation_comment=_("Initial balance"), ) # Create this also prevent the deletion of the very first customer BankAccount.objects.create( operation_date=timezone.now().date(), customer=very_first_customer, operation_comment=_("Initial balance"), ) @classmethod def get_closest_to(cls, target): # https://stackoverflow.com/questions/15855715/filter-on-datetime-closest-to-the-given-datetime # https://www.vinta.com.br/blog/2017/advanced-django-querying-sorting-events-date/ # Get closest bank_account (sub-)total from target date qs = cls.objects.filter(producer__isnull=True, customer__isnull=True).order_by( "?" ) closest_greater_qs = qs.filter(operation_date__gt=target).order_by( "operation_date" ) closest_less_qs = qs.filter(operation_date__lt=target).order_by( "-operation_date" ) closest_greater = closest_greater_qs.first() if closest_greater is None: closest_greater = closest_less_qs.first() closest_less = closest_less_qs.first() if closest_less is None: closest_less = closest_greater_qs.first() if closest_greater is not None and closest_less is not None: if ( closest_greater.operation_date - target > target - closest_less.operation_date ): return closest_less else: return closest_greater def get_bank_amount_in(self): if self.operation_status in [BANK_PROFIT, BANK_TAX]: return format_html( "<i>{}</i>", self.bank_amount_in if self.bank_amount_in.amount != DECIMAL_ZERO else EMPTY_STRING, ) else: return ( self.bank_amount_in if self.bank_amount_in.amount != DECIMAL_ZERO else EMPTY_STRING ) get_bank_amount_in.short_description = _("Cash in") get_bank_amount_in.admin_order_field = "bank_amount_in" def get_bank_amount_out(self): if self.operation_status in [BANK_PROFIT, BANK_TAX]: return format_html( "<i>{}</i>", self.bank_amount_out if self.bank_amount_out.amount != DECIMAL_ZERO else EMPTY_STRING, ) else: return ( self.bank_amount_out if self.bank_amount_out.amount != DECIMAL_ZERO else EMPTY_STRING ) get_bank_amount_out.short_description = _("Cash out") get_bank_amount_out.admin_order_field = "bank_amount_out" def get_producer(self): if self.producer is not None: return self.producer.short_profile_name else: if self.customer is None: # This is a total, show it if self.operation_status == BANK_LATEST_TOTAL: return format_html( "<b>=== {}</b>", settings.REPANIER_SETTINGS_GROUP_NAME ) else: return format_html( "<b>--- {}</b>", settings.REPANIER_SETTINGS_GROUP_NAME ) return EMPTY_STRING get_producer.short_description = _("Producer") get_producer.admin_order_field = "producer" def get_customer(self): if self.customer is not None: return self.customer.short_basket_name else: if self.producer is None: # This is a total, show it from repanier.apps import REPANIER_SETTINGS_BANK_ACCOUNT if self.operation_status == BANK_LATEST_TOTAL: if REPANIER_SETTINGS_BANK_ACCOUNT is not None: return format_html("<b>{}</b>", REPANIER_SETTINGS_BANK_ACCOUNT) else: return format_html("<b>{}</b>", "==============") else: if REPANIER_SETTINGS_BANK_ACCOUNT is not None: return format_html("<b>{}</b>", REPANIER_SETTINGS_BANK_ACCOUNT) else: return format_html("<b>{}</b>", "--------------") return EMPTY_STRING get_customer.short_description = _("Customer") get_customer.admin_order_field = "customer" class Meta: verbose_name = _("Bank account transaction") verbose_name_plural = _("Bank account transactions") ordering = ("-operation_date", "-id") index_together = [ ["operation_date", "id"], ["customer_invoice", "operation_date", "id"], ["producer_invoice", "operation_date", "operation_date", "id"], ["permanence", "customer", "producer", "operation_date", "id"], ]
class OfferItem(Item): translations = TranslatedFields( long_name=models.CharField(_("Long name"), max_length=100, default=EMPTY_STRING, blank=True, null=True), cache_part_a=HTMLField(default=EMPTY_STRING, blank=True), cache_part_b=HTMLField(default=EMPTY_STRING, blank=True), # Language dependant customer sort order for optimization order_sort_order=models.IntegerField(default=0, db_index=True), # Language dependant preparation sort order for optimization preparation_sort_order=models.IntegerField(default=0, db_index=True), # Language dependant producer sort order for optimization producer_sort_order=models.IntegerField(default=0, db_index=True) ) permanence = models.ForeignKey( 'Permanence', verbose_name=REPANIER_SETTINGS_PERMANENCE_NAME, on_delete=models.PROTECT, db_index=True ) product = models.ForeignKey( 'Product', verbose_name=_("Product"), on_delete=models.PROTECT) # is a box or a contract content is_box_content = models.BooleanField(default=False) producer_price_are_wo_vat = models.BooleanField(_("Producer price are without vat"), default=False) price_list_multiplier = models.DecimalField( _("Coefficient applied to the producer tariff to calculate the consumer tariff"), help_text=_("This multiplier is applied to each price automatically imported/pushed."), default=DECIMAL_ZERO, max_digits=5, decimal_places=4, validators=[MinValueValidator(0)]) is_resale_price_fixed = models.BooleanField( _("The resale price is set by the producer"), default=False) # Calculated with Purchase : Total producer purchase price vat included total_purchase_with_tax = ModelMoneyField( _("Producer amount invoiced"), default=DECIMAL_ZERO, max_digits=8, decimal_places=2) # Calculated with Purchase : Total customer selling price vat included total_selling_with_tax = ModelMoneyField( _("Invoiced to the consumer including tax"), default=DECIMAL_ZERO, max_digits=8, decimal_places=2) # Calculated with Purchase : Quantity invoiced to all customers # If Permanence.status < SEND this is the order quantity # During sending the orders to the producer this become the invoiced quantity # via permanence.recalculate_order_amount(..., send_to_producer=True) quantity_invoiced = models.DecimalField( _("Qty invoiced"), max_digits=9, decimal_places=4, default=DECIMAL_ZERO) use_order_unit_converted = models.BooleanField(default=False) may_order = models.BooleanField(_("May order"), default=True) manage_replenishment = models.BooleanField(_("Manage replenishment"), default=False) manage_production = models.BooleanField(default=False) producer_pre_opening = models.BooleanField(_("Pre-open the orders"), default=False) add_2_stock = models.DecimalField( _("Additional"), default=DECIMAL_ZERO, max_digits=9, decimal_places=4) new_stock = models.DecimalField( _("Remaining stock"), default=None, max_digits=9, decimal_places=3, null=True) contract = models.ForeignKey( 'Contract', verbose_name=_("Commitment"), on_delete=models.PROTECT, null=True, blank=True, default=None ) permanences_dates = models.TextField( null=True, blank=True, default=None) # Opposite of permaneces_date used to know when the related product is not into offer not_permanences_dates = models.TextField( null=True, blank=True, default=None) # Number of permanences where this product is placed. # Used to compute the price during order phase permanences_dates_counter = models.IntegerField( null=True, blank=True, default=1) # Important : permanences_dates_order is used to # group together offer item's of the same product of a contract # with different purchases dates on the order form # 0 : No group needed # 1 : Master of a group # > 1 : Displayed with the master of the group permanences_dates_order = models.IntegerField(default=0) def get_vat_level(self): return self.get_vat_level_display() get_vat_level.short_description = (_("VAT level")) get_vat_level.admin_order_field = 'vat_level' def get_producer_qty_stock_invoiced(self): # Return quantity to buy to the producer and stock used to deliver the invoiced quantity if self.quantity_invoiced > DECIMAL_ZERO: if self.manage_replenishment: # if RepanierSettings.producer_pre_opening then the stock is the max available qty by the producer, # not into our stock quantity_for_customer = self.quantity_invoiced - self.add_2_stock if self.stock == DECIMAL_ZERO: return self.quantity_invoiced, DECIMAL_ZERO, quantity_for_customer else: delta = (quantity_for_customer - self.stock).quantize(FOUR_DECIMALS) if delta <= DECIMAL_ZERO: # i.e. quantity_for_customer <= self.stock return self.add_2_stock, quantity_for_customer, quantity_for_customer else: return delta + self.add_2_stock, self.stock, quantity_for_customer else: return self.quantity_invoiced, DECIMAL_ZERO, self.quantity_invoiced return DECIMAL_ZERO, DECIMAL_ZERO, DECIMAL_ZERO def get_html_producer_qty_stock_invoiced(self): invoiced_qty, taken_from_stock, customer_qty = self.get_producer_qty_stock_invoiced() if invoiced_qty == DECIMAL_ZERO: if taken_from_stock == DECIMAL_ZERO: return EMPTY_STRING else: return mark_safe(_("Stock %(stock)s") % {'stock': number_format(taken_from_stock, 4)}) else: if taken_from_stock == DECIMAL_ZERO: return mark_safe(_("<b>%(qty)s</b>") % {'qty': number_format(invoiced_qty, 4)}) else: return mark_safe(_("<b>%(qty)s</b> + stock %(stock)s") % {'qty': number_format(invoiced_qty, 4), 'stock': number_format(taken_from_stock, 4)}) get_html_producer_qty_stock_invoiced.short_description = (_("Qty invoiced by the producer")) get_html_producer_qty_stock_invoiced.admin_order_field = 'quantity_invoiced' def get_producer_qty_invoiced(self): invoiced_qty, taken_from_stock, customer_qty = self.get_producer_qty_stock_invoiced() return invoiced_qty def get_producer_unit_price_invoiced(self): if self.producer_unit_price.amount > self.customer_unit_price.amount: return self.customer_unit_price else: return self.producer_unit_price def get_producer_row_price_invoiced(self): if self.manage_replenishment: if self.producer_unit_price.amount > self.customer_unit_price.amount: return RepanierMoney( (self.customer_unit_price.amount + self.unit_deposit.amount) * self.get_producer_qty_invoiced(), 2) else: return RepanierMoney( (self.producer_unit_price.amount + self.unit_deposit.amount) * self.get_producer_qty_invoiced(), 2) else: if self.producer_unit_price.amount > self.customer_unit_price.amount: return self.total_selling_with_tax else: return self.total_purchase_with_tax def get_html_producer_price_purchased(self): if self.manage_replenishment: invoiced_qty, taken_from_stock, customer_qty = self.get_producer_qty_stock_invoiced() price = RepanierMoney( ((self.producer_unit_price.amount + self.unit_deposit.amount) * invoiced_qty).quantize(TWO_DECIMALS)) else: price = self.total_purchase_with_tax if price != DECIMAL_ZERO: return mark_safe(_("<b>%(price)s</b>") % {'price': price}) return EMPTY_STRING get_html_producer_price_purchased.short_description = (_("Producer amount invoiced")) get_html_producer_price_purchased.admin_order_field = 'total_purchase_with_tax' def get_html_like(self, user): return mark_safe("<span class=\"glyphicon glyphicon-heart{}\" onclick=\"like_ajax({});return false;\"></span>".format( EMPTY_STRING if self.product.likes.filter(id=user.id).only("id").exists() else "-empty", self.id)) @cached_property def get_not_permanences_dates(self): if self.not_permanences_dates: all_dates_str = sorted( list(filter(None, self.not_permanences_dates.split(settings.DJANGO_SETTINGS_DATES_SEPARATOR)))) all_days = [] for one_date_str in all_dates_str: one_date = parse_date(one_date_str) all_days.append(one_date.strftime(settings.DJANGO_SETTINGS_DAY_MONTH)) return ", ".join(all_days) return EMPTY_STRING @cached_property def get_html_permanences_dates(self): if self.permanences_dates: all_dates_str = sorted( list(filter(None, self.permanences_dates.split(settings.DJANGO_SETTINGS_DATES_SEPARATOR)))) all_days = [] month_save = None for one_date_str in all_dates_str: one_date = parse_date(one_date_str) if month_save != one_date.month: if month_save is not None: new_line = "<br>" else: new_line = EMPTY_STRING month_save = one_date.month else: new_line = EMPTY_STRING all_days.append("{}{}".format(new_line, one_date.strftime(settings.DJANGO_SETTINGS_DAY_MONTH))) return mark_safe(", ".join(all_days)) return EMPTY_STRING @cached_property def get_permanences_dates(self): if self.permanences_dates: all_dates_str = sorted( list(filter(None, self.permanences_dates.split(settings.DJANGO_SETTINGS_DATES_SEPARATOR)))) all_days = [] # https://stackoverflow.com/questions/3845423/remove-empty-strings-from-a-list-of-strings # -> list(filter(None, all_dates_str)) for one_date_str in all_dates_str: one_date = parse_date(one_date_str) all_days.append("{}".format(one_date.strftime(settings.DJANGO_SETTINGS_DAY_MONTH))) return ", ".join(all_days) return EMPTY_STRING def get_order_name(self): qty_display = self.get_qty_display() if qty_display: return "{} {}".format(self.safe_translation_getter('long_name', any_language=True), qty_display) return "{}".format(self.safe_translation_getter('long_name', any_language=True)) def get_qty_display(self): if self.is_box: # To avoid unicode error in email_offer.send_open_order qty_display = BOX_UNICODE else: if self.use_order_unit_converted: # The only conversion done in permanence concerns PRODUCT_ORDER_UNIT_PC_KG # so we are sure that self.order_unit == PRODUCT_ORDER_UNIT_PC_KG qty_display = self.get_display( qty=1, order_unit=PRODUCT_ORDER_UNIT_KG, for_customer=False, without_price_display=True ) else: qty_display = self.get_display( qty=1, order_unit=self.order_unit, for_customer=False, without_price_display=True ) return qty_display def get_long_name(self, customer_price=True, is_html=False): if self.permanences_dates: new_line = "<br>" if is_html else "\n" return "{}{}{}".format( super(OfferItem, self).get_long_name(customer_price=customer_price), new_line, self.get_permanences_dates ) else: return super(OfferItem, self).get_long_name(customer_price=customer_price) def get_html_long_name(self): return mark_safe(self.get_long_name(is_html=True)) def get_long_name_with_producer(self, is_html=False): if self.permanences_dates: return "{}, {}".format( self.producer.short_profile_name, self.get_long_name(customer_price=True, is_html=is_html) ) else: return super(OfferItem, self).get_long_name_with_producer() def get_html_long_name_with_producer(self): return mark_safe(self.get_long_name_with_producer(is_html=True)) get_html_long_name_with_producer.short_description = (_("Offer items")) get_html_long_name_with_producer.allow_tags = True get_html_long_name_with_producer.admin_order_field = 'translations__long_name' def __str__(self): return self.get_long_name_with_producer() class Meta: verbose_name = _("Offer item") verbose_name_plural = _("Offer items") unique_together = ("permanence", "product", "permanences_dates")
class OfferItem(Item): translations = TranslatedFields( long_name=models.CharField(_("long_name"), max_length=100, default=EMPTY_STRING, blank=True, null=True), cache_part_a=HTMLField(default=EMPTY_STRING, blank=True), cache_part_b=HTMLField(default=EMPTY_STRING, blank=True), order_sort_order=models.IntegerField( _("customer sort order for optimization"), default=0, db_index=True), preparation_sort_order=models.IntegerField( _("preparation sort order for optimization"), default=0, db_index=True), producer_sort_order=models.IntegerField( _("producer sort order for optimization"), default=0, db_index=True)) permanence = models.ForeignKey( 'Permanence', verbose_name=REPANIER_SETTINGS_PERMANENCE_NAME, on_delete=models.PROTECT, db_index=True) product = models.ForeignKey('Product', verbose_name=_("product"), on_delete=models.PROTECT) # is a box or a contract content is_box_content = models.BooleanField(_("is a box content"), default=False) producer_price_are_wo_vat = models.BooleanField( _("producer price are wo vat"), default=False) price_list_multiplier = models.DecimalField( _("price_list_multiplier"), help_text= _("This multiplier is applied to each price automatically imported/pushed." ), default=DECIMAL_ZERO, max_digits=5, decimal_places=4, validators=[MinValueValidator(0)]) is_resale_price_fixed = models.BooleanField( _("the resale price is set by the producer"), default=False) # Calculated with Purchase total_purchase_with_tax = ModelMoneyField( _("producer amount invoiced"), help_text=_('Total purchase amount vat included'), default=DECIMAL_ZERO, max_digits=8, decimal_places=2) # Calculated with Purchase total_selling_with_tax = ModelMoneyField( _("customer amount invoiced"), help_text=_('Total selling amount vat included'), default=DECIMAL_ZERO, max_digits=8, decimal_places=2) # Calculated with Purchase. # If Permanence.status < SEND this is the order quantity # During sending the orders to the producer this become the invoiced quantity # via tools.recalculate_order_amount(..., send_to_producer=True) quantity_invoiced = models.DecimalField( _("quantity invoiced"), help_text=_('quantity invoiced to our customer'), max_digits=9, decimal_places=4, default=DECIMAL_ZERO) may_order = models.BooleanField(_("may_order"), default=True) manage_replenishment = models.BooleanField(_("manage stock"), default=False) manage_production = models.BooleanField(_("manage production"), default=False) producer_pre_opening = models.BooleanField(_("producer pre-opening"), default=False) add_2_stock = models.DecimalField(_("Add 2 stock"), default=DECIMAL_ZERO, max_digits=9, decimal_places=4) new_stock = models.DecimalField(_("Final stock"), default=None, max_digits=9, decimal_places=3, null=True) def get_vat_level(self): return self.get_vat_level_display() get_vat_level.short_description = EMPTY_STRING get_vat_level.allow_tags = False get_vat_level.admin_order_field = 'vat_level' def get_producer_qty_stock_invoiced(self): # Return quantity to buy to the producer and stock used to deliver the invoiced quantity if self.quantity_invoiced > DECIMAL_ZERO: if self.manage_replenishment: # if RepanierSettings.producer_pre_opening then the stock is the max available qty by the producer, # not into our stock quantity_for_customer = self.quantity_invoiced - self.add_2_stock if self.stock == DECIMAL_ZERO: return self.quantity_invoiced, DECIMAL_ZERO, quantity_for_customer else: delta = (quantity_for_customer - self.stock).quantize(FOUR_DECIMALS) if delta <= DECIMAL_ZERO: # i.e. quantity_for_customer <= self.stock return self.add_2_stock, quantity_for_customer, quantity_for_customer else: return delta + self.add_2_stock, self.stock, quantity_for_customer else: return self.quantity_invoiced, DECIMAL_ZERO, self.quantity_invoiced return DECIMAL_ZERO, DECIMAL_ZERO, DECIMAL_ZERO def get_html_producer_qty_stock_invoiced(self): invoiced_qty, taken_from_stock, customer_qty = self.get_producer_qty_stock_invoiced( ) if invoiced_qty == DECIMAL_ZERO: if taken_from_stock == DECIMAL_ZERO: return EMPTY_STRING else: return _("stock %(stock)s") % { 'stock': number_format(taken_from_stock, 4) } else: if taken_from_stock == DECIMAL_ZERO: return _("<b>%(qty)s</b>") % { 'qty': number_format(invoiced_qty, 4) } else: return _("<b>%(qty)s</b> + stock %(stock)s") % { 'qty': number_format(invoiced_qty, 4), 'stock': number_format(taken_from_stock, 4) } get_html_producer_qty_stock_invoiced.short_description = ( _("quantity invoiced by the producer")) get_html_producer_qty_stock_invoiced.allow_tags = True get_html_producer_qty_stock_invoiced.admin_order_field = 'quantity_invoiced' def get_producer_qty_invoiced(self): invoiced_qty, taken_from_stock, customer_qty = self.get_producer_qty_stock_invoiced( ) return invoiced_qty def get_producer_unit_price_invoiced(self): if self.producer_unit_price.amount > self.customer_unit_price.amount: return self.customer_unit_price else: return self.producer_unit_price def get_producer_row_price_invoiced(self): if self.manage_replenishment: if self.producer_unit_price.amount > self.customer_unit_price.amount: return RepanierMoney((self.customer_unit_price.amount + self.unit_deposit.amount) * self.get_producer_qty_invoiced(), 2) else: return RepanierMoney((self.producer_unit_price.amount + self.unit_deposit.amount) * self.get_producer_qty_invoiced(), 2) else: if self.producer_unit_price.amount > self.customer_unit_price.amount: return self.total_selling_with_tax else: return self.total_purchase_with_tax def get_html_producer_price_purchased(self): if self.manage_replenishment: invoiced_qty, taken_from_stock, customer_qty = self.get_producer_qty_stock_invoiced( ) price = RepanierMoney( ((self.producer_unit_price.amount + self.unit_deposit.amount) * invoiced_qty).quantize(TWO_DECIMALS)) else: price = self.total_purchase_with_tax # price = self.total_purchase_with_tax if price != DECIMAL_ZERO: return _("<b>%(price)s</b>") % {'price': price} return EMPTY_STRING get_html_producer_price_purchased.short_description = ( _("producer amount invoiced")) get_html_producer_price_purchased.allow_tags = True get_html_producer_price_purchased.admin_order_field = 'total_purchase_with_tax' def get_like(self, user): return '<span class="glyphicon glyphicon-heart%s" onclick="like_ajax(%d);return false;"></span>' % ( EMPTY_STRING if self.product.likes.filter( id=user.id).only("id").exists() else "-empty", self.id) def __str__(self): return super(OfferItem, self).display() # return '%s, %s' % (self.producer.short_profile_name, self.get_long_name()) class Meta: verbose_name = _("offer's item") verbose_name_plural = _("offer's items") unique_together = ( "permanence", "product", ) index_together = [["id", "order_unit"]]