4.销货(sale)模块
4.1.models.py 设定
4.1.1.订单
订单模块是本项目最起始的模块,有了订单之后才会有接下去的所有动作,本模块一样分成单头与单身。
CAT_CHOICES = [
('1', 'CAT1'),
('2', 'CAT2'),
('3', 'CAT3'),
]
ORDER_CHOICES = [
('N', u'尚未出货'),
('A', u'全部出货'),
('P', u'部分出货'),
('O', u'超额出货'),
]
class Order(models.Model):
cat_id = models.CharField(max_length=2, choices=CAT_CHOICES, blank=False, verbose_name=u'Cat.')
pi_no = models.CharField(max_length=16, verbose_name='PI #', blank=False, null=False, unique=True)
po_no = models.CharField(max_length=16, verbose_name='PO #', blank=True)
customer = models.ForeignKey(Customer, verbose_name=u'客户', on_delete=models.PROTECT,
limit_choices_to={'status': 'A'},)
is_active = models.BooleanField(verbose_name=u'是否有效', default=True)
is_urgency = models.BooleanField(verbose_name=u'是否为急单', default=False)
status = models.CharField(max_length=1, choices=ORDER_CHOICES, default='N', verbose_name=u'状态')
etd = models.DateField(blank=False, null=False, verbose_name=u'预定出货日期')
created = models.DateTimeField(auto_now_add=True, verbose_name=u'订购时间')
create_user = models.ForeignKey('auth.User', verbose_name=u'建立人员', on_delete=models.PROTECT)
def get_absolute_url(self):
return reverse('sale:order_detail', kwargs={'pk': self.pk})
def __str__(self):
return 'Id:{}-PI #:{}- PO #:{}'.format(self.id, self.pi_no, self.po_no)
class Meta:
verbose_name = '订单'
verbose_name_plural = verbose_name
permissions = (
('active_order', '可中止订单'),
)
4.1.2.订单单身
class OrderProduct(models.Model):
num = models.CharField(max_length=2, default='', verbose_name=u'单身项次')
order = models.ForeignKey(Order, verbose_name=u'订单单头', on_delete=models.CASCADE)
product = models.ForeignKey(Product, verbose_name=u'商品', on_delete=models.DO_NOTHING,
limit_choices_to={'status': 'A'},)
quantity = models.PositiveIntegerField(blank=False, verbose_name=u'订购数量')
price = models.DecimalField(max_digits=16, decimal_places=4, blank=False, verbose_name=u'未税金额')
tax = models.DecimalField(max_digits=16, decimal_places=4, blank=False, verbose_name=u'税金')
tax_price = models.DecimalField(max_digits=16, decimal_places=4, blank=False, verbose_name=u'含税金额')
subtotal = models.DecimalField(max_digits=16, decimal_places=4, blank=False, verbose_name=u'小计未税金额')
tax_subtotal = models.DecimalField(max_digits=16, decimal_places=4, blank=False, verbose_name=u'小计含税金额')
currency = models.ForeignKey(Currency, verbose_name=u'币别', blank=False, on_delete=models.DO_NOTHING)
description = models.CharField(max_length=256, verbose_name=u'描述', blank=True)
def save(self, *args, **kwargs):
if self.num == '':
#项次的格式是01,02...
order_products = OrderProduct.objects.filter(order=self.order)
num = "{0:02d}".format(order_products.count() + 1)
self.num = "{0:02d}".format(int(num))
product = Product.objects.get(title=self.product)
self.price = product.price
self.tax = product.tax
self.currency = product.currency
self.tax_price = product.tax_price
self.subtotal = product.price * self.quantity
self.tax_subtotal = product.tax_price * self.quantity
super().save(*args, **kwargs)
def __str__(self):
return '{}'.format(self.num)
class Meta:
verbose_name = '订单单身'
verbose_name_plural = verbose_name
4.1.3.出货单
接到订单后,如果商品库存有的话则可以安排出给客户,如果没有的话则要使用制程单制作好商品后出货,出货单一样分单头单身。
class Ship(models.Model):
order = models.ForeignKey(Order, verbose_name=u'对应订单', on_delete=models.PROTECT)
customer = models.ForeignKey(Customer, verbose_name=u'客户', on_delete=models.PROTECT,
limit_choices_to={'status': 'A'})
is_active = models.BooleanField(verbose_name=u'是否有效', default=False)
is_delay = models.BooleanField(verbose_name=u'是否延迟', default=False)
created = models.DateTimeField(auto_now_add=True, verbose_name=u'出货时间')
create_user = models.ForeignKey('auth.User', verbose_name=u'建立人员', on_delete=models.PROTECT)
def get_absolute_url(self):
return reverse('sale:ship_detail', kwargs={'pk': self.pk})
def __str__(self):
return '{}'.format(self.id)
class Meta:
verbose_name = '出货单'
verbose_name_plural = verbose_name
permissions = (
('active_ship', '可中止出货单'),
)
4.1.4.出货单单身
class ShipDetail(models.Model):
num = models.CharField(max_length=2, default='', verbose_name=u'单身项次')
ship = models.ForeignKey(Ship, verbose_name=u'出货单单头', on_delete=models.CASCADE)
product = models.ForeignKey(Product, verbose_name=u'出货商品', on_delete=models.CASCADE)
quantity = models.PositiveIntegerField(blank=False, verbose_name=u'出货数量')
description = models.CharField(max_length=256, verbose_name=u'描述', blank=True)
def save(self, *args, **kwargs):
if self.num == '':
#项次的格式是01,02...
ship_detail = ShipDetail.objects.filter(ship=self.ship)
num = "{0:02d}".format(ship_detail.count() + 1)
self.num = "{0:02d}".format(int(num))
super().save(*args, **kwargs)
def __str__(self):
return '{}'.format(self.num)
class Meta:
verbose_name = '出货单单身'
verbose_name_plural = verbose_name
4.2.admin.py 设定
本模块的admin设定也相对简单,所以也不多说明了。
from django import forms
from django.contrib import admin, messages
from django.contrib.auth import get_permission_codename
import datetime
from .models import Order, OrderProduct, Ship, ShipDetail
from finance.models import Receivable, ReceivableDetail
from inventory.models import Ptran
"""
订单单身检查
1.最少要有一个单身
2.商品不可重复
3.商品数量是否大于0
"""
class OrderProductCheckInlineFormset(forms.models.BaseInlineFormSet):
def clean(self):
count = 0
product_list = []
for form in self.forms:
if form.cleaned_data:
count += 1
quantity = form.cleaned_data.get('quantity')
if quantity <= 0:
raise forms.ValidationError(u'订单中商品数量不得为0或是负数。')
product_id = form.cleaned_data.get('product')
if product_id in product_list:
raise forms.ValidationError(u'订单中商品不得重复。')
else:
product_list.append(product_id)
if count < 1:
raise forms.ValidationError(u'您最少必须输入一笔订单单身。')
class OrderProductInline(admin.TabularInline):
formset = OrderProductCheckInlineFormset
model = OrderProduct
fields = ['product', 'quantity', 'description']
raw_id_fields = ['product']
extra = 0
class OrderAdmin(admin.ModelAdmin):
list_display = ['id', 'cat_id', 'pi_no', 'po_no', 'customer', 'is_urgency', 'status',
'etd', 'created', 'create_user']
fields = ['cat_id', 'po_no', 'customer', 'is_urgency', 'etd']
list_filter = ['is_urgency', 'status']
actions = ['make_actived']
inlines = [OrderProductInline]
view_on_site = False
list_per_page = 10
list_max_show_all = 100
date_hierarchy = 'created'
def save_model(self, request, obj, form, change):
if not change:
obj.create_user = request.user
"""
pi_no的格式是R201901121
R:开头
201901:2019年1月
121:三码流水码
"""
pattern = 'R' + datetime.datetime.now().strftime("%Y%m")
orders = Order.objects.filter(pi_no__startswith=pattern)
obj.pi_no = pattern + "{0:03d}".format(orders.count() + 1)
super().save_model(request, obj, form, change)
def make_actived(self, request, queryset):
rows = queryset.update(is_active=False)
if rows > 0:
self.message_user(request, u'已完成终止订单动作')
make_actived.allowed_permissions = ('active',)
make_actived.short_description = u'终止订单'
def has_active_permission(self, request):
opts = self.opts
codename = get_permission_codename('active', opts)
return request.user.has_perm('%s.%s' % (opts.app_label, codename))
admin.site.register(Order, OrderAdmin)
"""
出货单单身检查
1.最少要有一个单身
2.商品库存是否足够
3.出货数量是否至少一笔大于0
4.出货数量不得是负数
4.单身商品不得重复
"""
class ShipDetailCheckInlineFormset(forms.models.BaseInlineFormSet):
def clean(self):
detail_count = 0
detail_amount = 0
product_list = []
for form in self.forms:
if form.cleaned_data:
detail_count += 1
quantity = form.cleaned_data.get('quantity')
if quantity < 0:
raise forms.ValidationError(u'出货单中商品数量不得是负数。')
elif quantity > 0:
detail_amount += 1
product = form.cleaned_data.get('product')
quantity = int(form.cleaned_data.get('quantity'))
if product.stock < quantity:
raise forms.ValidationError(u"商品[{}-{}]库存 {},无法满足此次出货量 {},"
u"请先填写制程单增加商品库存。".format(product.id, product.title,
product.stock, quantity))
if product.id in product_list:
raise forms.ValidationError(u"单身商品[{}-{}]已重复,"
u"请重新填写出货单。".format(product.id, product.title))
else:
product_list.append(product.id)
if detail_count < 1:
raise forms.ValidationError(u'您必须最少输入一笔订单单身')
if detail_amount < 1:
raise forms.ValidationError(u'您必须最少一笔出货单单身数量大于0')
class ShipDetailInline(admin.TabularInline):
formset = ShipDetailCheckInlineFormset
model = ShipDetail
fields = ['product', 'quantity', 'description']
raw_id_fields = ['product']
extra = 0
"""
出货时的相关动作如下:
1.对于每个出货单单身而言
i.新增 商品库存异动(Ptran)
ii.如果出货数量>0时则新增 应收帐款单身(ReceivableDetail)
iii.减少 商品库存量(product.stock)
2.对于整个出货单而言
i.将出货单的 状态修改为有效出货(is_active=True) 检查出货日期是否有延迟(is_delay)
ii.如果出货数量>0时则新增一笔 应收帐款(Receivable)
iii.检查订单数量是否满足,决定订单"状态",('A', '全部出货'),('P', '部分出货')
"""
class ShipAdmin(admin.ModelAdmin):
list_display = ['id', 'order', 'customer', 'is_active', 'is_delay', 'created', 'create_user']
fields = ['order']
actions = ['make_actived']
inlines = [ShipDetailInline]
view_on_site = False
list_per_page = 10
list_max_show_all = 100
date_hierarchy = 'created'
def formfield_for_foreignkey(self, db_field, request, **kwargs):
if db_field.name == 'order':
kwargs['queryset'] = Order.objects.filter(is_active=True).exclude(status='A')
return super(ShipAdmin, self).formfield_for_foreignkey(db_field, request, **kwargs)
def save_model(self, request, obj, form, change):
if not change:
obj.create_user = request.user
order = obj.order
obj.customer = order.customer
super().save_model(request, obj, form, change)
def save_related(self, request, form, formsets, change):
super().save_related(request, form, formsets, change)
ship = form.save(commit=False)
#新增应收帐款单头
receivable = Receivable()
receivable.ship = ship
receivable.customer = ship.customer
#应收日期=出货日期 + 帐期
receivable.receivabled = datetime.datetime.now().date() + datetime.timedelta(days=ship.customer.period.period)
receivable.create_user = ship.create_user
receivable.save()
ship_details = ShipDetail.objects.filter(ship=ship)
for ship_detail in ship_details:
#新增商品库存异动
product = ship_detail.product
stock = product.stock
tran_quantity = ship_detail.quantity
ptran = Ptran()
ptran.product = product
ptran.source_form = 'SHIP'
ptran.source_id = ship.id
ptran.from_quantity = stock
ptran.tran_quantity = 0 - tran_quantity
ptran.to_quantity = stock - tran_quantity
ptran.create_user = request.user
ptran.save()
#新增应收帐款单身
receivable_detail = ReceivableDetail()
receivable_detail.receivable = receivable
receivable_detail.product = product
receivable_detail.amount = product.tax_price * tran_quantity
receivable_detail.currency = product.currency
receivable_detail.save()
#减少商品库存量
product.stock = stock - tran_quantity
product.save()
order = form.cleaned_data.get('order')
#修改出货单状态
ship.is_active = True
today = datetime.datetime.now().date()
if today > order.etd:
ship.is_delay = True
ship.save()
#决定对应订单状态('A', '全部出货'),('P', '部分出货'),('O', '超额出货')如果有一个订单单身数量没有全部出完的话,则状态为P
order_products = OrderProduct.objects.filter(order=order)
ships = Ship.objects.filter(order=order)
status = 'A'
for order_product in order_products:
product = order_product.product
shipped_quantity = 0
for ship in ships.all():
ship_details = ShipDetail.objects.filter(product=product, ship=ship)
if ship_details.all().count() > 0:
for ship_detail in ship_details.all():
shipped_quantity += ship_detail.quantity
if shipped_quantity < order_product.quantity:
status = 'P'
else:
if shipped_quantity > order_product.quantity:
#如果有商品是部分出货P,就算是有超额出货也还是算成P
if status is 'P':
status = 'P'
else:
status = 'O'
order.status = status
order.save()
#如果没有发生错误则修改应收帐款状态
receivable.is_active = True
receivable.save()
def make_actived(self, request, queryset):
rows = queryset.update(is_active=False)
if rows > 0:
self.message_user(request, u'已完成终止出货单动作')
make_actived.allowed_permissions = ('active',)
make_actived.short_description = u'终止出货单'
def has_active_permission(self, request):
opts = self.opts
codename = get_permission_codename('active', opts)
return request.user.has_perm('%s.%s' % (opts.app_label, codename))
admin.site.register(Ship, ShipAdmin)
4.3.templates 设定
销货模块里面只有订单与出货单两个主体,出货单我希望的呈现方式如下:
1.先让使用者选择对应的订单
2.选择好对应订单后再显示其他字段,并在单身中带出订单单身
使用的方式一样是jquery(change_form.html)与views.py,位置与代码如下:
{% extends "admin/base_site.html" %}
{% load i18n admin_urls static admin_modify %}
{% block extrahead %}{{ block.super }}
<script type="text/javascript" src="{% url 'admin:jsi18n' %}"></script>
{{ media }}
{% endblock %}
{% block extrastyle %}{{ block.super }}<link rel="stylesheet" type="text/css" href="{% static "admin/css/forms.css" %}">{% endblock %}
{% block coltype %}colM{% endblock %}
{% block bodyclass %}{{ block.super }} app-{{ opts.app_label }} model-{{ opts.model_name }} change-form{% endblock %}
{% if not is_popup %}
{% block breadcrumbs %}
<div class="breadcrumbs">
<a href="{% url 'admin:index' %}">{% trans 'Home' %}</a>
› <a href="{% url 'admin:app_list' app_label=opts.app_label %}">{{ opts.app_config.verbose_name }}</a>
› {% if has_view_permission %}<a href="{% url opts|admin_urlname:'changelist' %}">{{ opts.verbose_name_plural|capfirst }}</a>{% else %}{{ opts.verbose_name_plural|capfirst }}{% endif %}
› {% if add %}{% blocktrans with name=opts.verbose_name %}Add {{ name }}{% endblocktrans %}{% else %}{{ original|truncatewords:"18" }}{% endif %}
</div>
{% endblock %}
{% endif %}
{% block content %}<div id="content-main">
{% block object-tools %}
{% if change %}{% if not is_popup %}
<ul class="object-tools">
{% block object-tools-items %}
{% change_form_object_tools %}
{% endblock %}
</ul>
{% endif %}{% endif %}
{% endblock %}
<form {% if has_file_field %}enctype="multipart/form-data" {% endif %}action="{{ form_url }}" method="post" id="{{ opts.model_name }}_form" novalidate>{% csrf_token %}{% block form_top %}{% endblock %}
<div>
{% if is_popup %}<input type="hidden" name="{{ is_popup_var }}" value="1">{% endif %}
{% if to_field %}<input type="hidden" name="{{ to_field_var }}" value="{{ to_field }}">{% endif %}
{% if save_on_top %}{% block submit_buttons_top %}{% submit_row %}{% endblock %}{% endif %}
{% if errors %}
<p class="errornote">
{% if errors|length == 1 %}{% trans "Please correct the error below." %}{% else %}{% trans "Please correct the errors below." %}{% endif %}
</p>
{{ adminform.form.non_field_errors }}
{% endif %}
{% block field_sets %}
{% for fieldset in adminform %}
{% include "./includes/fieldset.html" %}
{% endfor %}
{% endblock %}
{% block after_field_sets %}{% endblock %}
{% block inline_field_sets %}
{% for inline_admin_formset in inline_admin_formsets %}
{% include inline_admin_formset.opts.template %}
{% endfor %}
{% endblock %}
{% block after_related_objects %}{% endblock %}
{% block submit_buttons_bottom %}{% submit_row %}{% endblock %}
{% block admin_change_form_document_ready %}
<script type="text/javascript"
id="django-admin-form-add-constants"
src="{% static 'admin/js/change_form.js' %}"
{% if adminform and add %}
data-model-name="{{ opts.model_name }}"
{% endif %}>
</script>
{% endblock %}
{# JavaScript for prepopulated fields #}
{% prepopulated_fields_js %}
<script>
(function($) {
$(document).ready(function(){
var $form = $('#ship_form');
var formset_name = 'shipdetail_set';
var $formset_div = $('#' + formset_name + '-group');
var $formset_table = $formset_div.find('table');
var $formset_add_row_tr = $formset_div.find('tr.add-row')
var $submit_row_div = $form.find('div.submit-row');
var $formset_empty_row = $formset_table.find('tbody tr#' + formset_name + '-empty');
var $formset_total_input = $formset_div.find('input#id_' + formset_name + '-TOTAL_FORMS')
$formset_add_row_tr.hide();
$submit_row_div.hide();
var href = location.href;
href_list = href.split('/');
<!-- 表示为change -->
if (href_list[href_list.length - 2] == 'change'){
$('div.submit-row').html('<a href="/admin/sale/ship/" class="closelink">Close</a>');
$('div.submit-row').show();
}
if($('#id_order').val() == '')
{
<!-- 把非"对应订单"的输入字段隐藏起来 -->
$form.find('fieldset:first>div').each(function(){
if(!$(this).hasClass('field-order')){
$(this).hide();
}
})
<!-- 隐藏"formset",""储存窗体按钮"" -->
$formset_div.hide();
$submit_row_div.hide();
<!-- 选择"对应订单"之后 -->
$('#id_order').change(function(){
var order_id = $(this).val();
if(order_id != ''){
$(this).attr("disabled","disabled");
<!-- 移除已有的出货单身 -->
<!-- $formset_div.find('table tbody .form-row').remove(); -->
<!-- 根据对应订单产生出货单身 -->
$.get('{% url 'sale:order_ajax_product_list' %}', {'id':order_id}, function(data){
if(data['code'] == 0){
$form.find('fieldset:first>div').each(function(){
if(!$(this).hasClass('field-order')){
$(this).show();
}
})
$formset_div.show();
$submit_row_div.show();
$submit_row_div.find('input').each(function(){
$(this).click(function(){
$('#id_order').removeAttr("disabled");
});
});
<!-- 将数据填入窗体中 -->
$tbody = $formset_table.find('tbody');
var products = data['products'];
var index = 0;
for(p in products){
total = $formset_total_input.val();
$new_row = $formset_empty_row.clone(true);
$formset_empty_row.removeClass('row' + (index + 1) % 3);
$formset_empty_row.addClass('row' + (index + 2) % 3);
$new_row.attr('id', formset_name + '-' + total);
$new_row.removeClass('empty-form');
$new_row.find('input').each(function(){
name = $(this).attr('name');
id = $(this).attr('id');
$(this).attr('name', name.replace(/__prefix__/, total));
$(this).attr('id', id.replace(/__prefix__/, total));
name_list = name.split('-');
if(name_list[name_list.length -1] == "product"){
$(this).val(products[p]['product']['id']);
$(this).attr('readonly', 'readonly');
$(this).parent().append(products[p]['product']['title']);
}
else if(name_list[name_list.length -1] == "quantity"){
$(this).val(products[p]['quantity']);
}
});
$new_row.find('a').remove();
$formset_empty_row.before($new_row);
total++;
$formset_total_input.val(total);
index++;
}
if (data['msg'] != '')
alert(data['msg']);
}
else{
alert(data['msg']);
}
}, 'json')
}
});
}
else{
$('#id_order').attr("disabled","disabled");
}
});
})(django.jQuery);
</script>
</div>
</form></div>
{% endblock %}
4.4.urls.py 设定
from django.urls import path
from django.contrib.auth.decorators import login_required
from . import views
app_name = 'sale'
urlpatterns = [
path('order/ajax/list/', views.order_ajax_product_list, name='order_ajax_product_list'),
]
4.5.views.py 设定
from django.http import HttpResponse, JsonResponse, HttpResponseRedirect
from .models import Order, OrderProduct, Ship, ShipDetail
def order_ajax_product_list(request):
return_dict = {}
#判断用户是否有权限检视订单
if request.user.has_perm('sale.view_order'):
order_id = request.GET.get('id')
order = Order.objects.get(id=order_id)
return_dict['code'] = 0
return_dict['msg'] = ''
return_dict['products'] = []
order_products = OrderProduct.objects.filter(order=order)
for p in order_products.all():
product = p.product
quantity = p.quantity
# 如果该订单有其他对应出货单,则需扣掉已出货的商品数量
ships = Ship.objects.filter(order=order, is_active=True)
if ships.count() > 0:
return_dict['msg'] = u'此订单已有对应出货单,出货单单号如下:'
for s in ships.all():
return_dict['msg'] += ' ' + str(s.id)
ship_product = ShipDetail.objects.filter(ship=s, product=product)
if ship_product.all().count() > 0:
quantity -= ship_product[0].quantity
product_info = {}
product_info['id'] = product.id
product_info['title'] = product.title
product_dict = {'product': product_info, 'quantity': quantity}
return_dict['products'].append(product_dict)
else:
return_dict['code'] = 1
return_dict['msg'] = u"您无权限浏览订单"
return JsonResponse(return_dict)