일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | ||||||
2 | 3 | 4 | 5 | 6 | 7 | 8 |
9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 | 17 | 18 | 19 | 20 | 21 | 22 |
23 | 24 | 25 | 26 | 27 | 28 |
- Python
- pip 설치
- django erp
- QApplication
- pyside6
- 파이썬
- uiload
- optimization page
- materialized
- test drive development
- pyside6 ui
- 페이지 최적화
- django
- orm 최적화
- query 최적화
- 장고
- qpa_plugin
- ERP
- 중량 관리
- channels
- tensorflow
- django drf
- 장고로 ERP
- django rank
- qwindows.dll
- pip 오류
- django test
- 재고 관리
- Self ERP
- django role based
- Today
- Total
취미삼아 배우는 프로그래밍
Django로 개인 업무(ERP) 홈페이지 만들기-7 본문
Django로 개인 업무(ERP) 홈페이지 만들기-6
진행상황 어쩌다보니 테이블을 되게 정성스럽게 만들었다. def order_manage(request): template_name = "order_list_page.html" qs = IntegratedInfo.objects.filter(is_completed=False).order_by('rank_number..
nadure.tistory.com
이전에 만들던거에 이어서 만들어본다.
지금은 어느정도 진행을 했기 때문에, 완성된 코드를 보여주는것 밖에 못하지만 진짜 우여곡절이 많았다.
모델
models.py
from django.db import models
from django.db.models.fields import related
from django.utils.timezone import now
# Create your models here.
class Locations(models.Model):
location = models.CharField(max_length=10,
verbose_name="출고지")
def __str__(self):
return self.location
class Categories(models.Model):
category = models.CharField(max_length=20,
verbose_name="구분")
def __str__(self):
return self.category
class SubCategoies(models.Model):
sub_category = models.CharField(max_length=20,
verbose_name="서포트 세부구분")
def __str__(self):
return self.sub_category
class DetailedReceiptInformation(models.Model):
category = models.ForeignKey(
Categories,
on_delete=models.CASCADE,
help_text="카테고리")
sub_category = models.ForeignKey(
SubCategoies,
null=True, blank=True,
on_delete=models.CASCADE, help_text="서포트일 경우")
weight = models.IntegerField(default=0,
verbose_name="중량")
is_steel = models.BooleanField(default=False,
blank=True,verbose_name="스틸유무")
is_alu = models.BooleanField(default=False,
blank=True, verbose_name="알루유무")
is_support = models.BooleanField(default=False,
blank=True, verbose_name="서포트유무")
class IntegratedReceiptInformation(models.Model):
from_location = models.ForeignKey(
Locations,
on_delete=models.CASCADE,
related_name="from_location",
null=True, blank=True,)
to_location = models.ForeignKey(
Locations,
on_delete=models.CASCADE,
related_name="to_location",
null=True, blank=True,)
information = models.OneToOneField(DetailedReceiptInformation,
on_delete=models.CASCADE,
blank=True, null=True,)
registered_date = models.DateField(verbose_name="등록일",
default=now, blank=True)
memo = models.CharField(max_length=50, verbose_name="메모",
blank=True, null=True,)
모델자체는 아주 간단하게 짰다.
폼
폼 코드
forms.py
class ReceiptInputdataForm(forms.Form):
location_list = []
locs = Locations.objects.all()
for loc in locs:
location_list.append((loc.location, loc.location))
category_list = []
categories = Categories.objects.all()
for cat in categories:
category_list.append((cat.category, cat.category))
sub_category_list = [('', '----')]
sub_categories = SubCategoies.objects.all()
for sub_cat in sub_categories:
sub_category_list.append(( sub_cat.sub_category, sub_cat.sub_category))
TYPE_CHOICES=[
('alu','알루미늄'),
('st','스틸'),
('support','서포트'),
('no','해당없음'),
]
from_location = forms.ChoiceField(choices=location_list, label='출고장소', required=True)
to_location = forms.ChoiceField(choices=location_list, label='반입장소', required=True)
category = forms.ChoiceField(choices=category_list, label='카테고리', required=True)
sub_category = forms.ChoiceField(choices=sub_category_list, label='서브 카테고리', required=False)
registered_date = forms.DateField(label='등록일', required=True,)
weight = forms.IntegerField(min_value=0, label='중량', required=True)
type_check = forms.ChoiceField(choices=TYPE_CHOICES, label='유형', required=True)
memo = forms.CharField(max_length=50, label='메모', required=False)
'''
TODO:
validation error occurs::
같은 출고지일시,
서포트인데 서브카테고리 없음
서포트 아닌데 서브카테고리 있음
'''
def clean(self):
cleaned_data = super().clean()
from_location = cleaned_data.get("from_location")
to_location = cleaned_data.get("to_location")
if from_location == to_location:
raise ValidationError(
"출고지와 입고지가 같을 순 없습니다."
)
특이사항이라면,
clean 을 오버라이딩 했는 점과 location_list, category_list, sub_category_list를 데이터베이스로부터 불러와서 폼을 짜 사용할 수 있도록 했다.
그 외의 validation 에러들을 넣을 필요가 있다.
데이터 입력 페이지
데이터 입력 페이지 코드
views.py : input_data_page(request)
def input_data_page(request):
template_name = "receipt/input_data.html"
if request.method == 'POST':
form = ReceiptInputdataForm(request.POST)
if form.is_valid():
# django transaction
# 적용해볼것
# https://blueshw.github.io/2016/01/16/django-migration/
#
from_location = form.cleaned_data['from_location']
to_location = form.cleaned_data['to_location']
category = form.cleaned_data['category']
sub_category = form.cleaned_data['sub_category']
registered_date = form.cleaned_data['registered_date']
weight = form.cleaned_data['weight']
type_check = form.cleaned_data['type_check']
memo = form.cleaned_data['memo']
is_alu, is_steel, is_support = False, False, False
if type_check == 'alu':
is_alu = True
elif type_check == 'st':
is_steel = True
elif type_check == 'support':
is_support = True
else:
type_check = None
cat = Categories.objects.get(category=category)
from_location = Locations.objects.get(location=from_location)
to_location = Locations.objects.get(location=to_location)
try:
sub_cat = SubCategoies.objects.get(sub_category=sub_category)
except:
sub_cat = None
with transaction.atomic():
detailed = DetailedReceiptInformation(
category_id = cat.id,
sub_category_id = sub_cat,
weight = weight,
is_alu = is_alu,
is_steel = is_steel,
is_support = is_support,
)
detailed.save()
integrated = IntegratedReceiptInformation(
from_location_id = from_location.id,
to_location_id = to_location.id,
registered_date = registered_date,
information_id = detailed.id,
memo = memo,
)
integrated.save()
return redirect('receipt_input_data')
else :
form = ReceiptInputdataForm()
context = {
'form':form
}
return render(request, template_name, context)
특이사항은 atomic을 사용했다는 점이다.
지금과 같은 여러 모델들을 경유해서 통합된 모델을 저장할 때는 천천히 밑에서부터 타고가면서 저장을 해야하는데, 하나가 삑사리가 나면 끝에까지 저장이 안되고 도중에 저장됐던건 그대로 있게된다.
그래서, 이 atomic을 쓰게 되면 중간에 삑사리가 나게되면 저장 과정에서 삑사리가 나기 전까지의 내용들은 모두 무효로 한다. 이걸 원자성을 보존하는거라고 한다.
쿼리셋
근 1주일동안 고생했던 부분이다. 아까 해결됐다..
코드
queryset.py
class SumQueryset(object):
def __init__(self, to_location):
self.date = datetime.date(2020, 11, 1) # 나중에 인자로 받아야함.
self.end_day_of_month = calendar.monthrange(self.date.year, self.date.month)[1]
self.to_location = to_location
self.res = self.checkout_all_data()
def _combine_columns_with_category(self, input_dict):
res = []
previous_dict = {}
for i,k in zip(input_dict[0::1], input_dict[1::1]):
if compare_equal(i,k):
previous_dict.update(i)
previous_dict.update(k)
if input_dict[len(input_dict)-1] == k:
res.append(previous_dict)
else:
if not previous_dict:
res.append(i)
elif input_dict[0] == i:
res.append(i)
if previous_dict:
res.append(previous_dict)
if input_dict[len(input_dict)-1] == k:
res.append(k)
previous_dict = {}
return res
def checkout_all_data(self):
locations = Locations.objects.all()
result = {}
for loc in locations:
# res += self._one_row_queryset_annotate(from_location=loc)
if self.to_location == loc.location:
continue
res = self._one_row_queryset_annotate(from_location=loc)
if not res:
continue
df = pd.DataFrame.from_dict(res)
tt = df.groupby(['category', 'registered_date']).sum().reset_index()
res_record = tt.to_dict('records')
for l in res_record:
date = l.get('registered_date')
weight = l.get('sum_of_weight')
l['weight_'+str(date.day)] = weight
l['registered_date_'+str(date.day)] = date
l['out_place'] = loc.location
new_dict = {}
if len(res_record) == 1:
result[str(loc.location)] = res_record
else:
result[str(loc.location)] = self._combine_columns_with_category(res_record)
return result
def _get_sum_annotation(self, from_location):
to_location = self.to_location
return {
'sum_of_weight': Sum(
Case(
When(
Q(detailedreceiptinformation__integratedreceiptinformation__to_location__location=to_location) and
Q(detailedreceiptinformation__integratedreceiptinformation__from_location__location=from_location),
then=F('detailedreceiptinformation__weight')
),
# 입고따로 출고따로 해야함
# When(
# Q(detailedreceiptinformation__integratedreceiptinformation__from_location__location=to_location),
# then=F('detailedreceiptinformation__weight')*-1 # 빼주기 위해 곱함),
default=0, output_field=IntegerField()
)
),
'registered_date': F('detailedreceiptinformation__integratedreceiptinformation__registered_date'),
# 'from_location': F('detailedreceiptinformation__integratedreceiptinformation__from_location__location'),
# 'to_location': F('detailedreceiptinformation__integratedreceiptinformation__to_location__location'),
}
def _filter_data(self):
to_location = Locations.objects.get(location=self.to_location)
# to_location(입고) - from_location(출고)
# Queryresult : bottom >> top.
year, month = self.date.year, self.date.month
month_range = calendar.monthrange(year, month)
Q_year = Q(detailedreceiptinformation__integratedreceiptinformation__registered_date__year=year)
Q_month = Q(detailedreceiptinformation__integratedreceiptinformation__registered_date__month=month)
Q_daterange = Q(
detailedreceiptinformation__integratedreceiptinformation__registered_date__range=[
Q(datetime.date(year, month, 1),
datetime.date(year, month, month_range[1]))
]
)
res = Categories.objects.filter(
Q_year and Q_month
)
return res
def _one_row_queryset_annotate(self, from_location):
res = self._filter_data()
res_list = []
res = res.annotate(
**self._get_sum_annotation(from_location)
)
res = res.order_by("registered_date")
res_list = list(
res.values(
"registered_date",
"sum_of_weight",
"category",
# "from_location",
# "to_location"
)
)
# if list(res_list['sum_of_weight']) == 0:
# return None
filtered_res = []
for dt in res_list:
if dt['sum_of_weight'] != 0:
filtered_res.append(dt)
return filtered_res
눈여겨 볼 부분은 queryset을 정말 더럽게 했는것이다.
플로우는 이렇다.
- 쿼리셋을 요청해서 받는다.
- 쿼리셋 결과는 요청하는 일자에 따라서 변경될 수 있게끔 되게 했다. (dict['weight_' datetime.day] 와 같은 변수형식)
- 그러면 쿼리 결과 값은 category 들로 쫙 뜨게된다.
- 운이 없게도 쿼리 결과 값은 정렬이 안돼있다. 지들끼리 막 섞여있는데 참담하기 그지없다.
- 그래서 pandas를 동원해서 sum을 하되, registered_date와 category 컬럼은 살렸다.
- 그치만 여기서 더 애속하게도 각각의 쿼리값은 하나의 weight 값만을 갖고있다.
- 예를 들자면 10일치의 category 가 스크랩인 결과값이 있으면, 행이 10개인 데이터셋이 돼버린다.(사실 한 행짜리 데이터셋이어야 한다.)
이건 pandas 어머니 아버지가 와서 도와줘도 힘들것 같았다.- 자체 합치는 알고리즘을 짰다.
- 중간에 django_tables2를 사용하기위해서 억지로 몇 가지 값들을 넣어줬다.
어떻게 억지로 기워맞춰서 django_tables2로도 사용할 수 있게 포맷을 변경했다. 어떻게 하다보니 하다하다 도저히 같은 category값을 가진 dict들의 합을 구하려다 안되서 pandas까지 사용했다. 사실 사용안하고 싶었다.
그렇게 해서 만들어진 dash_baord와 table
우선 대쉬보드를 보자면,
def dashboard_page(request):
template_name = "receipt/dashboard.html"
# cached data 사용해서 리소스 사용 줄일 것(로딩할 데이터 엄청 많아짐,)
cd = datetime.datetime.now()
end_day = calendar.monthrange(cd.year, cd.month)
columns_list = []
locations = Locations.objects.all()
for i in range(1, end_day[1]+1):
columns_list.append(
('registered_date_'+str(i), WeightColumn(verbose_name=i),)
)
to_location = '광혜원'
table_data = SumQueryset(to_location=to_location)
table_list = []
for loc in locations:
_data = table_data.res.get(loc.location, None)
if not _data:
continue
table = SummaryTable(_data, extra_columns=columns_list)
table_list.append(table)
context = {
'table_list':table_list,
'to_location':to_location,
}
return render(request, template_name, context)
다른건 뭐 별볼일 없는것 같고, WeightColumn과 extra_columns를 사용했다는 점이다. 이중에 WeightColum은 django_tables2의 Column을 오버라이딩해서 입맛에 맞추었다.
그다음은 테이블
import django_tables2 as tables
# id, data-value등을 넘겨주기 쉽게 하기 위해
# 따로 컬럼을 오버라이딩 함
class CategoryColumn(tables.Column):
def render(self, value):
return value
class WeightColumn(tables.Column):
def render(self, value, record):
if value.day == self.verbose_name:
self.attrs = {
'td':{
'data-year':value.year,
'data-month':value.month,
'data-day':value.day,
'data-category':record['category']
}
}
return record.get('weight_'+str(value.day), '')
else:
return ''
class OutPlaceColumn(tables.Column):
def render(self, value, record):
return value
class SummaryTable(tables.Table):
out_place = OutPlaceColumn(verbose_name="출고지")
category = CategoryColumn(verbose_name="품목")
장고 테이블을 사용할 때는 고생해서 다큐먼트 보면서 거기에 끼워맞추지 말고, 본인의 입맛에 맞게 오버라이딩하는게 시간상 최고인 것 같다.
이렇게 코드가 더럽게 많은데 속도가 안 느린게 더 신기하다.
'파이썬(장고)' 카테고리의 다른 글
Django로 개인 업무(ERP) 홈페이지 만들기-6 (0) | 2020.11.16 |
---|---|
Django로 개인 업무(ERP) 홈페이지 만들기-5 (0) | 2020.11.14 |
Django로 개인 업무(ERP) 홈페이지 만들기-4 (0) | 2020.11.13 |
Django로 개인 업무(ERP) 홈페이지 만들기-3 (0) | 2020.11.10 |
Django로 개인 업무(ERP) 홈페이지 만들기-2 (0) | 2020.11.03 |