diff --git a/apps/pm/filters.py b/apps/pm/filters.py index 2c57673d..038de12c 100644 --- a/apps/pm/filters.py +++ b/apps/pm/filters.py @@ -22,7 +22,6 @@ class MtaskFilter(filters.FilterSet): "material_out__type": ["exact"], "material_out__is_hidden": ["exact"], "mgroup__belong_dept__name": ["exact"], - "parent": ["exact", "isnull"] } def filter_tag(self, queryset, name, value): diff --git a/apps/pm/migrations/0007_auto_20231019_1812.py b/apps/pm/migrations/0007_auto_20231019_1812.py new file mode 100644 index 00000000..400065f7 --- /dev/null +++ b/apps/pm/migrations/0007_auto_20231019_1812.py @@ -0,0 +1,55 @@ +# Generated by Django 3.2.12 on 2023-10-19 10:12 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion +import django.utils.timezone + + +class Migration(migrations.Migration): + + dependencies = [ + ('mtm', '0017_auto_20231018_1033'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('pm', '0006_auto_20231016_1648'), + ] + + operations = [ + migrations.RemoveField( + model_name='mtask', + name='parent', + ), + migrations.AlterField( + model_name='mtask', + name='state', + field=models.PositiveIntegerField(choices=[(10, '创建中'), (20, '已下达'), (30, '生产中'), (40, '已提交')], default=10, help_text="((10, '创建中'), (20, '已下达'), (30, '生产中'), (40, '已提交'))", verbose_name='状态'), + ), + migrations.CreateModel( + name='Utask', + fields=[ + ('id', models.CharField(editable=False, help_text='主键ID', max_length=20, primary_key=True, serialize=False, verbose_name='主键ID')), + ('create_time', models.DateTimeField(default=django.utils.timezone.now, help_text='创建时间', verbose_name='创建时间')), + ('update_time', models.DateTimeField(auto_now=True, help_text='修改时间', verbose_name='修改时间')), + ('is_deleted', models.BooleanField(default=False, help_text='删除标记', verbose_name='删除标记')), + ('state', models.PositiveIntegerField(choices=[(10, '创建中'), (20, '已下达'), (30, '生产中'), (40, '已提交')], default=10, help_text="((10, '创建中'), (20, '已下达'), (30, '生产中'), (40, '已提交'))", verbose_name='状态')), + ('number', models.CharField(max_length=50, unique=True, verbose_name='编号')), + ('count', models.PositiveIntegerField(default=1, verbose_name='任务数')), + ('count_real', models.PositiveIntegerField(default=0, verbose_name='实际生产数')), + ('count_ok', models.PositiveIntegerField(default=0, verbose_name='合格数')), + ('count_notok', models.PositiveIntegerField(default=0, verbose_name='不合格数')), + ('start_date', models.DateField(verbose_name='计划开工日期')), + ('end_date', models.DateField(verbose_name='计划完工日期')), + ('create_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='utask_create_by', to=settings.AUTH_USER_MODEL, verbose_name='创建人')), + ('material', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='mtm.material', verbose_name='产品')), + ('update_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='utask_update_by', to=settings.AUTH_USER_MODEL, verbose_name='最后编辑人')), + ], + options={ + 'abstract': False, + }, + ), + migrations.AddField( + model_name='mtask', + name='utask', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='pm.utask', verbose_name='关联大任务'), + ), + ] diff --git a/apps/pm/models.py b/apps/pm/models.py index 67c016f9..1053bf8b 100644 --- a/apps/pm/models.py +++ b/apps/pm/models.py @@ -5,6 +5,33 @@ from apps.mtm.models import Material, Mgroup # Create your models here. +class Utask(CommonADModel): + """ + 生产大任务 + """ + UTASK_CREATED = 10 + UTASK_ASSGINED = 20 + UTASK_WORKING = 30 + UTASK_DONE = 40 + UTASK_STATES = ( + (UTASK_CREATED, '创建中'), + (UTASK_ASSGINED, '已下达'), + (UTASK_WORKING, '生产中'), + (UTASK_DONE, '已提交') + ) + state = models.PositiveIntegerField( + '状态', choices=UTASK_STATES, default=UTASK_CREATED, help_text=str(UTASK_STATES)) + number = models.CharField('编号', max_length=50, unique=True) + material = models.ForeignKey( + Material, verbose_name='产品', on_delete=models.CASCADE) + count = models.PositiveIntegerField('任务数', default=1) + count_real = models.PositiveIntegerField('实际生产数', default=0) + count_ok = models.PositiveIntegerField('合格数', default=0) + count_notok = models.PositiveIntegerField('不合格数', default=0) + start_date = models.DateField('计划开工日期') + end_date = models.DateField('计划完工日期') + + class Mtask(CommonADModel): """ 生产任务 @@ -16,7 +43,7 @@ class Mtask(CommonADModel): MTASK_STATES = ( (MTASK_CREATED, '创建中'), (MTASK_ASSGINED, '已下达'), - # (MTASK_WORKING, '生产中'), + (MTASK_WORKING, '生产中'), (MTASK_DONE, '已提交') ) state = models.PositiveIntegerField( @@ -34,5 +61,5 @@ class Mtask(CommonADModel): count_notok = models.PositiveIntegerField('不合格数', default=0) start_date = models.DateField('计划开工日期') end_date = models.DateField('计划完工日期') - parent = models.ForeignKey( - 'self', null=True, blank=True, on_delete=models.SET_NULL, verbose_name='父任务') + utask = models.ForeignKey( + Utask, verbose_name='关联大任务', on_delete=models.CASCADE, null=True, blank=True) diff --git a/apps/pm/serializers.py b/apps/pm/serializers.py index 3c9c4e1d..3cd64167 100644 --- a/apps/pm/serializers.py +++ b/apps/pm/serializers.py @@ -2,17 +2,16 @@ from rest_framework import serializers from rest_framework.exceptions import ValidationError from apps.mtm.serializers import MaterialSerializer -from apps.pm.models import Mtask +from apps.pm.models import Mtask, Utask from apps.sam.models import OrderItem from apps.utils.serializers import CustomModelSerializer -class MtaskSerializer(CustomModelSerializer): +class UtaskSerializer(CustomModelSerializer): material_ = MaterialSerializer(source='material', read_only=True) - mgroup_name = serializers.CharField(source='mgroup.name', read_only=True) class Meta: - model = Mtask + model = Utask fields = '__all__' def update(self, instance, validated_data): @@ -23,9 +22,28 @@ class MtaskSerializer(CustomModelSerializer): return super().update(instance, new_data) +class MtaskSerializer(CustomModelSerializer): + material_ = MaterialSerializer(source='material', read_only=True) + mgroup_name = serializers.CharField(source='mgroup.name', read_only=True) + + class Meta: + model = Mtask + fields = '__all__' + read_only_fields = ['utask'] + + def update(self, instance, validated_data): + if instance.state != Mtask.MTASK_CREATED: + raise ValidationError('任务非创建中不可编辑') + if instance.utask is not None: + new_data = {'count': validated_data['count']} + else: + new_data = {key: validated_data[key] for key in [ + 'number', 'count', 'start_date', 'end_date']} + return super().update(instance, new_data) + + class SchedueSerializer(serializers.Serializer): orderitems = serializers.PrimaryKeyRelatedField( label='orderitem的ID列表', queryset=OrderItem.objects.all(), many=True) start_date = serializers.DateField(label='计划开工日期') - end_date = serializers.DateField( - label='计划完工日期', allow_null=True, required=False) + end_date = serializers.DateField(label='计划完工日期') diff --git a/apps/pm/services.py b/apps/pm/services.py index 9771bc5f..89b99920 100644 --- a/apps/pm/services.py +++ b/apps/pm/services.py @@ -1,7 +1,7 @@ from apps.sam.models import OrderItem from apps.mtm.models import Route, Material, Mgroup from rest_framework.exceptions import ParseError -from apps.pm.models import Mtask +from apps.pm.models import Mtask, Utask from django.db.models.query import QuerySet from datetime import date, timedelta import math @@ -10,9 +10,103 @@ from typing import List class PmService: + @classmethod + def make_utasks_from_orderitems(cls, user, orderitemIds: List[str], start_date: date, end_date: date): + start_date_str = start_date.strftime('%Y%m%d') + if start_date >= end_date: + raise ParseError('开始时间不可大于结束时间') + orderitems = OrderItem.objects.filter(pk__in=orderitemIds) + if orderitems.exclude(utask=None).exists(): + raise ParseError('存在订单项已排任务!') + rdict = {} + for item in orderitems: + productId = item.material.id + if productId not in rdict: + rdict[productId] = [item.material, item.count, [item.id]] + else: + rdict[productId][1] = rdict[productId][1] + item.count + rdict[productId][2].append(item.id) + # 生成大任务 + make_list = [] + for k, v in rdict.items(): + xproduct, xcount, orderitemsId = v + if xproduct.is_assemb: + for key in xproduct.components: + make_list.append([Material.objects.get( + id=key), xcount*xproduct.components[key], orderitemsId]) + else: + make_list = [[xproduct, xcount, orderitemsId]] + for i in make_list: + material, count, orderitemsId = i + utask = Utask.objects.create( + number=f'{material.number}_{start_date_str}', + material=material, + count=count, + start_date=start_date, + end_date=end_date, + create_by=user, + update_by=user + ) + OrderItem.objects.filter(id__in=orderitemIds).update(utask=utask) + + @classmethod + def schedue_mtasks(cls, user, utask: Utask): + """ + 从大任务自动排产出小任务 + """ + number, product, count, start_date, end_date = utask.number, utask.material, utask.count, utask.start_date, utask.end_date + # 计算相差天数 + rela_days = (end_date - start_date).days + 1 + # 获取每个产品的加工路线 + rqs = Route.get_routes(product) + # 创建小任务 + for ind, val in enumerate(rqs): + if val.material_out: + halfgood = val.material_out + else: + raise ParseError(f'第{ind+1}步-无输出物料') + if val.material_in: + material_in = val.material_in + elif ind > 0: + material_in = rqs[ind-1].material_out + if val.is_autotask: + # 找到工段 + mgroups = Mgroup.objects.filter(process=val.process) + mgroups_count = mgroups.count() + if mgroups_count == 1: + mgroup = mgroups.first() + elif mgroups_count == 0: + raise ParseError(f'第{ind+1}步-工段不存在!') + else: # 后面可能会指定车间 + raise ParseError(f'第{ind+1}步-工段存在多个!') + task_count = count + if val.out_rate: + if val.out_rate > 1: + task_count = math.ceil( + count / (val.out_rate/100)) + else: + task_count = math.ceil(count / val.out_rate) + task_count_day = math.ceil(task_count/rela_days) + if rela_days > 1: + for i in range(rela_days): + task_date = start_date + timedelta(days=i) + Mtask.objects.create(**{ + 'number': f'{number}_r{ind+1}_{i+1}', + 'material_out': halfgood, + 'material_in': material_in, + 'mgroup': mgroup, + 'count': task_count_day, + 'start_date': task_date, + 'end_date': task_date, + 'utask': utask, + 'create_by': user, + 'update_by': user + }) + @classmethod def check_orderitems(cls, orderitems: QuerySet[OrderItem]): """ + 废弃 校验orderitems并返回整合后的字典以productId为key, [product, count, end_date, orderitems] 为value """ rdict = {} @@ -34,6 +128,7 @@ class PmService: @classmethod def schedue_from_orderitems(cls, user, orderitemIds: List[str], start_date: date, end_date: date = None): """ + 废弃 从多个订单明细自动排产 """ orderitems = OrderItem.objects.filter(pk__in=orderitemIds) diff --git a/apps/pm/urls.py b/apps/pm/urls.py index 725faae1..b75e9f9e 100644 --- a/apps/pm/urls.py +++ b/apps/pm/urls.py @@ -1,12 +1,13 @@ from django.urls import path, include from rest_framework.routers import DefaultRouter -from apps.pm.views import (MtaskViewSet) +from apps.pm.views import (MtaskViewSet, UtaskViewSet) API_BASE_URL = 'api/pm/' HTML_BASE_URL = 'pm/' router = DefaultRouter() router.register('mtask', MtaskViewSet, basename='mtask') +router.register('utask', UtaskViewSet, basename='utask') urlpatterns = [ path(API_BASE_URL, include(router.urls)), -] \ No newline at end of file +] diff --git a/apps/pm/views.py b/apps/pm/views.py index be93b2e0..5b40063a 100644 --- a/apps/pm/views.py +++ b/apps/pm/views.py @@ -7,13 +7,77 @@ from apps.utils.serializers import PkSerializer from apps.utils.viewsets import CustomModelViewSet from .filters import MtaskFilter -from .models import Mtask -from .serializers import MtaskSerializer, SchedueSerializer +from .models import Mtask, Utask +from .serializers import MtaskSerializer, SchedueSerializer, UtaskSerializer from .services import PmService # Create your views here. +class UtaskViewSet(CustomModelViewSet): + """ + list: 生产大任务 + + 生产大任务 + """ + queryset = Utask.objects.all() + serializer_class = UtaskSerializer + filterset_fields = ['material', 'state'] + select_related_fields = ['material'] + ordering = ['-start_date'] + + @action(methods=['post'], detail=False, perms_map={'post': 'utask.schedue'}, serializer_class=SchedueSerializer) + @transaction.atomic + def schedue_utasks(self, request, *args, **kwargs): + """从多个订单明细生成大任务 + + 从多个订单明细生成大任务 + """ + sr = SchedueSerializer(data=request.data) + sr.is_valid(raise_exception=True) + vdata = sr.validated_data + PmService.make_utasks_from_orderitems(request.user, + request.data['orderitems'], vdata['start_date'], vdata['end_date']) + return Response() + + @action(methods=['post'], detail=False, perms_map={'post': 'utask.schedue'}, serializer_class=PkSerializer) + @transaction.atomic + def schedue_mtasks(self, request, *args, **kwargs): + """大任务自动排产为小任务 + + 大任务自动排产为小任务 + """ + sr = PkSerializer(data=request.data) + sr.is_valid(raise_exception=True) + vdata = sr.validated_data + utasks = Utask.objects.filter( + id__in=vdata['ids'], state=Utask.UTASK_CREATED) + for i in utasks: + PmService.schedue_mtasks(request.user, i) + return Response() + + @action(methods=['post'], detail=False, perms_map={'post': 'utask.assgin'}, serializer_class=PkSerializer) + @transaction.atomic + def assgin(self, request): + """下达任务 + + 下达任务 + """ + ids = request.data.get('ids', []) + utasks = Utask.objects.filter(id__in=ids) + Mtask.objects.filter(utask__in=utasks, state=Mtask.MTASK_CREATED).update( + state=Mtask.MTASK_ASSGINED) + utasks.filter(state=Utask.UTASK_CREATED).update( + state=Utask.UTASK_ASSGINED) + # 此处要更新订单状态 + from apps.sam.models import OrderItem, Order + orderIds = OrderItem.objects.filter( + utask__in=utasks).values_list('order', flat=True).distinct() + Order.objects.filter(id__in=orderIds, state=Order.ORDER_SUBMITED).update( + state=Order.ORDER_DOING) + return Response() + + class MtaskViewSet(CustomModelViewSet): """ list: 生产任务 @@ -27,39 +91,25 @@ class MtaskViewSet(CustomModelViewSet): ordering_fields = ['start_date', 'mgroup__process__sort'] ordering = ['-start_date', 'mgroup__process__sort', '-create_time'] - @action(methods=['post'], detail=False, perms_map={'post': 'mtask.schedue'}, serializer_class=SchedueSerializer) - @transaction.atomic - def schedue_from_orderitems(self, request, *args, **kwargs): - """从多个订单明细自动排产 + # @action(methods=['post'], detail=False, perms_map={'post': 'mtask.schedue'}, serializer_class=SchedueSerializer) + # @transaction.atomic + # def schedue_from_orderitems(self, request, *args, **kwargs): + # """从多个订单明细自动排产 - 从多个订单明细自动排产 - """ - sr = SchedueSerializer(data=request.data) - sr.is_valid(raise_exception=True) - vdata = sr.validated_data - PmService.schedue_from_orderitems(request.user, - request.data['orderitems'], vdata['start_date'], vdata.get('end_date', None)) - return Response() + # 从多个订单明细自动排产 + # """ + # sr = SchedueSerializer(data=request.data) + # sr.is_valid(raise_exception=True) + # vdata = sr.validated_data + # PmService.schedue_from_orderitems(request.user, + # request.data['orderitems'], vdata['start_date'], vdata.get('end_date', None)) + # return Response() def perform_destroy(self, instance): if instance.state != Mtask.MTASK_CREATED: raise ParseError('该任务非创建中不可删除') return super().perform_destroy(instance) - @action(methods=['post'], detail=False, perms_map={'post': 'mtask.assgin'}, serializer_class=PkSerializer) - @transaction.atomic - def assgin(self, request): - """下达任务 - - 下达任务 - """ - ids = request.data.get('ids', []) - mtasks = Mtask.objects.filter( - id__in=ids, parent=None, state=Mtask.MTASK_CREATED) - mtasks = mtasks | Mtask.objects.filter(parent__in=mtasks) - mtasks.update(state=Mtask.MTASK_ASSGINED) - return Response() - @action(methods=['post'], detail=False, perms_map={'post': 'mtask.submit'}, serializer_class=PkSerializer) @transaction.atomic def submit(self, request): diff --git a/apps/sam/filters.py b/apps/sam/filters.py index 263e958d..f0cb4e28 100644 --- a/apps/sam/filters.py +++ b/apps/sam/filters.py @@ -9,5 +9,5 @@ class OrderItemFilter(filters.FilterSet): fields = { "order": ["exact", "in"], "order__state": ["exact", "in"], - "mtask": ["exact", "isnull"] + "utask": ["exact", "isnull"] } diff --git a/apps/sam/migrations/0005_auto_20231019_1812.py b/apps/sam/migrations/0005_auto_20231019_1812.py new file mode 100644 index 00000000..115f6b8d --- /dev/null +++ b/apps/sam/migrations/0005_auto_20231019_1812.py @@ -0,0 +1,24 @@ +# Generated by Django 3.2.12 on 2023-10-19 10:12 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('pm', '0007_auto_20231019_1812'), + ('sam', '0004_alter_order_state'), + ] + + operations = [ + migrations.RemoveField( + model_name='orderitem', + name='mtask', + ), + migrations.AddField( + model_name='orderitem', + name='utask', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='pm.utask', verbose_name='关联生产大任务'), + ), + ] diff --git a/apps/sam/models.py b/apps/sam/models.py index ed47ea92..ee3b5556 100644 --- a/apps/sam/models.py +++ b/apps/sam/models.py @@ -50,15 +50,13 @@ class Order(CommonBModel): """ ORDER_CREATE = 10 ORDER_SUBMITED = 20 - ORDER_PLANING = 30 - ORDER_PLANED = 40 - ORDER_DELIVERED = 50 + ORDER_DOING = 30 + ORDER_DELIVERED = 40 ORDER_STATES = ( (10, '创建中'), (20, '已提交'), - (30, '排产中'), - (40, '排产完成'), - (50, '已交付') + (30, '进行中'), + (40, '已交付') ) state = models.PositiveSmallIntegerField( '订单状态', default=ORDER_CREATE, choices=ORDER_STATES, help_text=str(ORDER_STATES)) @@ -87,5 +85,5 @@ class OrderItem(BaseModel): Material, verbose_name='所需产品', on_delete=models.CASCADE) count = models.PositiveIntegerField('所需数量', default=1) delivered_count = models.PositiveIntegerField('已交货数量', default=0) - mtask = models.ForeignKey('pm.mtask', verbose_name='关联生产任务', + utask = models.ForeignKey('pm.utask', verbose_name='关联生产大任务', on_delete=models.SET_NULL, null=True, blank=True) diff --git a/apps/sam/services.py b/apps/sam/services.py index 2a929979..7fcedc6f 100644 --- a/apps/sam/services.py +++ b/apps/sam/services.py @@ -3,6 +3,7 @@ from rest_framework.exceptions import ValidationError from django.db.models import F from apps.inm.models import MIO, MIOItem + class SamService: def mio_saleout(mio: MIO): @@ -18,9 +19,10 @@ class SamService: orderitem.delivered_count = delivered_count orderitem.save() # 更新order的状态 - qs = OrderItem.objects.filter(order=order, count__lte=F('delivered_count')) + qs = OrderItem.objects.filter( + order=order, count__lte=F('delivered_count')) if qs.exists(): pass else: order.state = Order.ORDER_DELIVERED - order.save() \ No newline at end of file + order.save() diff --git a/apps/sam/tasks.py b/apps/sam/tasks.py index 0914e8bd..b90fe6f7 100644 --- a/apps/sam/tasks.py +++ b/apps/sam/tasks.py @@ -5,18 +5,18 @@ from celery import shared_task from .models import Order, OrderItem -@shared_task(base=CustomTask) -def change_order_state_when_schedue(orderitemIds): - """排产后更新orderstate - """ - orderIds = list(OrderItem.objects.filter( - id__in=orderitemIds).values_list('order__id', flat=True).distinct()) - for i in orderIds: - order = Order.objects.get(id=i) - state = Order.ORDER_PLANED - orderitems = OrderItem.objects.filter(order__id=i) - for item in orderitems: - if item.mtask is None: - state = Order.ORDER_PLANING - order.state = state - order.save() +# @shared_task(base=CustomTask) +# def change_order_state_when_schedue(orderitemIds): +# """排产后更新orderstate +# """ +# orderIds = list(OrderItem.objects.filter( +# id__in=orderitemIds).values_list('order__id', flat=True).distinct()) +# for i in orderIds: +# order = Order.objects.get(id=i) +# state = Order.ORDER_PLANED +# orderitems = OrderItem.objects.filter(order__id=i) +# for item in orderitems: +# if item.mtask is None: +# state = Order.ORDER_PLANING +# order.state = state +# order.save() diff --git a/apps/wpm/services.py b/apps/wpm/services.py index e3b48061..8f0605b3 100644 --- a/apps/wpm/services.py +++ b/apps/wpm/services.py @@ -137,20 +137,23 @@ def mlog_confirm(mlog: Mlog): def update_mtask(mtask: Mtask): + from apps.pm.models import Utask res = Mlog.objects.filter(mtask=mtask).aggregate(sum_count_real=Sum( 'count_real'), sum_count_ok=Sum('count_ok'), sum_count_notok=Sum('count_notok')) mtask.count_real = res['sum_count_real'] if res['sum_count_real'] else 0 mtask.count_ok = res['sum_count_ok'] if res['sum_count_ok'] else 0 mtask.count_notok = res['sum_count_notok'] if res['sum_count_notok'] else 0 mtask.save() - if mtask.parent: - fmtask = mtask.parent - res2 = Mtask.objects.filter(parent=fmtask).aggregate(sum_count_real=Sum( + utask = mtask + if utask and mtask.material_out == utask.material: + res2 = Mtask.objects.filter(utask=utask, material_out=utask.material_out).aggregate(sum_count_real=Sum( 'count_real'), sum_count_ok=Sum('count_ok'), sum_count_notok=Sum('count_notok')) - fmtask.count_real = res2['sum_count_real'] if res2['sum_count_real'] else 0 - fmtask.count_ok = res2['sum_count_ok'] if res2['sum_count_ok'] else 0 - fmtask.count_notok = res2['sum_count_notok'] if res2['sum_count_notok'] else 0 - fmtask.save() - if Mtask.objects.filter(parent=fmtask).exclude(state=Mtask.MTASK_DONE).count() == 0: - fmtask.state = Mtask.MTASK_DONE - fmtask.save() + utask.count_real = res2['sum_count_real'] if res2['sum_count_real'] else 0 + utask.count_ok = res2['sum_count_ok'] if res2['sum_count_ok'] else 0 + utask.count_notok = res2['sum_count_notok'] if res2['sum_count_notok'] else 0 + if utask.count_ok > 0 and utask.state == Utask.UTASK_ASSGINED: + utask.state = Utask.UTASK_WORKING + utask.save() + if Mtask.objects.filter(utask=utask).exclude(state=Mtask.MTASK_DONE).count() == 0: + utask.state = Mtask.MTASK_DONE + utask.save()