Merge branch 'v12-pre-release' into version-12

This commit is contained in:
Sahil Khan
2019-12-20 15:55:12 +05:30
165 changed files with 5621 additions and 9813 deletions

View File

@@ -5,7 +5,7 @@ import frappe
from erpnext.hooks import regional_overrides from erpnext.hooks import regional_overrides
from frappe.utils import getdate from frappe.utils import getdate
__version__ = '12.2.2' __version__ = '12.3.0'
def get_default_company(user=None): def get_default_company(user=None):
'''Get default company for user''' '''Get default company for user'''

View File

@@ -109,12 +109,13 @@ class Account(NestedSet):
if not descendants: return if not descendants: return
parent_acc_name_map = {} parent_acc_name_map = {}
parent_acc_name = frappe.db.get_value('Account', self.parent_account, "account_name") parent_acc_name, parent_acc_number = frappe.db.get_value('Account', self.parent_account, \
["account_name", "account_number"])
for d in frappe.db.get_values('Account', for d in frappe.db.get_values('Account',
{"company": ["in", descendants], "account_name": parent_acc_name}, { "company": ["in", descendants], "account_name": parent_acc_name,
"account_number": parent_acc_number },
["company", "name"], as_dict=True): ["company", "name"], as_dict=True):
parent_acc_name_map[d["company"]] = d["name"] parent_acc_name_map[d["company"]] = d["name"]
if not parent_acc_name_map: return if not parent_acc_name_map: return
self.create_account_for_child_company(parent_acc_name_map, descendants, parent_acc_name) self.create_account_for_child_company(parent_acc_name_map, descendants, parent_acc_name)

View File

@@ -15,8 +15,8 @@ def upload_bank_statement():
with open(frappe.uploaded_file, "rb") as upfile: with open(frappe.uploaded_file, "rb") as upfile:
fcontent = upfile.read() fcontent = upfile.read()
else: else:
from frappe.utils.file_manager import get_uploaded_content fcontent = frappe.local.uploaded_file
fname, fcontent = get_uploaded_content() fname = frappe.local.uploaded_filename
if frappe.safe_encode(fname).lower().endswith("csv".encode('utf-8')): if frappe.safe_encode(fname).lower().endswith("csv".encode('utf-8')):
from frappe.utils.csvutils import read_csv_content from frappe.utils.csvutils import read_csv_content

View File

@@ -398,7 +398,7 @@ cur_frm.cscript.voucher_type = function(doc, cdt, cdn) {
method: "erpnext.accounts.doctype.journal_entry.journal_entry.get_default_bank_cash_account", method: "erpnext.accounts.doctype.journal_entry.journal_entry.get_default_bank_cash_account",
args: { args: {
"account_type": (doc.voucher_type=="Bank Entry" ? "account_type": (doc.voucher_type=="Bank Entry" ?
"Bank" : (doc.voucher_type=="Cash" ? "Cash" : null)), "Bank" : (doc.voucher_type=="Cash Entry" ? "Cash" : null)),
"company": doc.company "company": doc.company
}, },
callback: function(r) { callback: function(r) {

View File

@@ -931,9 +931,9 @@ def get_payment_entry(dt, dn, party_amount=None, bank_account=None, bank_amount=
grand_total = doc.rounded_total or doc.grand_total grand_total = doc.rounded_total or doc.grand_total
outstanding_amount = doc.outstanding_amount outstanding_amount = doc.outstanding_amount
elif dt in ("Expense Claim"): elif dt in ("Expense Claim"):
grand_total = doc.total_sanctioned_amount grand_total = doc.total_sanctioned_amount + doc.total_taxes_and_charges
outstanding_amount = doc.total_sanctioned_amount \ outstanding_amount = doc.grand_total \
- doc.total_amount_reimbursed - flt(doc.total_advance_amount) - doc.total_amount_reimbursed
elif dt == "Employee Advance": elif dt == "Employee Advance":
grand_total = doc.advance_amount grand_total = doc.advance_amount
outstanding_amount = flt(doc.advance_amount) - flt(doc.paid_amount) outstanding_amount = flt(doc.advance_amount) - flt(doc.paid_amount)

View File

@@ -350,13 +350,13 @@ def get_amount(ref_doc):
if dt in ["Sales Order", "Purchase Order"]: if dt in ["Sales Order", "Purchase Order"]:
grand_total = flt(ref_doc.grand_total) - flt(ref_doc.advance_paid) grand_total = flt(ref_doc.grand_total) - flt(ref_doc.advance_paid)
if dt in ["Sales Invoice", "Purchase Invoice"]: elif dt in ["Sales Invoice", "Purchase Invoice"]:
if ref_doc.party_account_currency == ref_doc.currency: if ref_doc.party_account_currency == ref_doc.currency:
grand_total = flt(ref_doc.outstanding_amount) grand_total = flt(ref_doc.outstanding_amount)
else: else:
grand_total = flt(ref_doc.outstanding_amount) / ref_doc.conversion_rate grand_total = flt(ref_doc.outstanding_amount) / ref_doc.conversion_rate
if dt == "Fees": elif dt == "Fees":
grand_total = ref_doc.outstanding_amount grand_total = ref_doc.outstanding_amount
if grand_total > 0 : if grand_total > 0 :

View File

@@ -389,8 +389,7 @@
"fieldname": "rate_or_discount", "fieldname": "rate_or_discount",
"fieldtype": "Select", "fieldtype": "Select",
"label": "Rate or Discount", "label": "Rate or Discount",
"options": "\nRate\nDiscount Percentage\nDiscount Amount", "options": "\nRate\nDiscount Percentage\nDiscount Amount"
"reqd": 1
}, },
{ {
"default": "Grand Total", "default": "Grand Total",
@@ -439,19 +438,20 @@
}, },
{ {
"default": "0", "default": "0",
"depends_on": "eval:!doc.mixed_conditions", "depends_on": "eval:!doc.mixed_conditions && doc.apply_on != 'Transaction'",
"fieldname": "same_item", "fieldname": "same_item",
"fieldtype": "Check", "fieldtype": "Check",
"label": "Same Item" "label": "Same Item"
}, },
{ {
"depends_on": "eval:!doc.same_item || doc.mixed_conditions", "depends_on": "eval:(!doc.same_item || doc.apply_on == 'Transaction') || doc.mixed_conditions",
"fieldname": "free_item", "fieldname": "free_item",
"fieldtype": "Link", "fieldtype": "Link",
"label": "Free Item", "label": "Free Item",
"options": "Item" "options": "Item"
}, },
{ {
"default": "0",
"fieldname": "free_qty", "fieldname": "free_qty",
"fieldtype": "Float", "fieldtype": "Float",
"label": "Qty" "label": "Qty"
@@ -554,7 +554,7 @@
], ],
"icon": "fa fa-gift", "icon": "fa fa-gift",
"idx": 1, "idx": 1,
"modified": "2019-10-15 12:39:40.399792", "modified": "2019-12-18 17:29:22.957077",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Pricing Rule", "name": "Pricing Rule",

View File

@@ -48,6 +48,9 @@ class PricingRule(Document):
if tocheck and not self.get(tocheck): if tocheck and not self.get(tocheck):
throw(_("{0} is required").format(self.meta.get_label(tocheck)), frappe.MandatoryError) throw(_("{0} is required").format(self.meta.get_label(tocheck)), frappe.MandatoryError)
if self.price_or_product_discount == 'Price' and not self.rate_or_discount:
throw(_("Rate or Discount is required for the price discount."), frappe.MandatoryError)
def validate_applicable_for_selling_or_buying(self): def validate_applicable_for_selling_or_buying(self):
if not self.selling and not self.buying: if not self.selling and not self.buying:
throw(_("Atleast one of the Selling or Buying must be selected")) throw(_("Atleast one of the Selling or Buying must be selected"))
@@ -181,8 +184,9 @@ def get_serial_no_for_item(args):
item_details.serial_no = get_serial_no(args) item_details.serial_no = get_serial_no(args)
return item_details return item_details
def get_pricing_rule_for_item(args, price_list_rate=0, doc=None): def get_pricing_rule_for_item(args, price_list_rate=0, doc=None, for_validate=False):
from erpnext.accounts.doctype.pricing_rule.utils import get_pricing_rules from erpnext.accounts.doctype.pricing_rule.utils import (get_pricing_rules,
get_applied_pricing_rules, get_pricing_rule_items, get_product_discount_rule)
if isinstance(doc, string_types): if isinstance(doc, string_types):
doc = json.loads(doc) doc = json.loads(doc)
@@ -209,6 +213,57 @@ def get_pricing_rule_for_item(args, price_list_rate=0, doc=None):
item_details, args.get('item_code')) item_details, args.get('item_code'))
return item_details return item_details
update_args_for_pricing_rule(args)
pricing_rules = (get_applied_pricing_rules(args)
if for_validate and args.get("pricing_rules") else get_pricing_rules(args, doc))
if pricing_rules:
rules = []
for pricing_rule in pricing_rules:
if not pricing_rule: continue
if isinstance(pricing_rule, string_types):
pricing_rule = frappe.get_cached_doc("Pricing Rule", pricing_rule)
pricing_rule.apply_rule_on_other_items = get_pricing_rule_items(pricing_rule)
if pricing_rule.get('suggestion'): continue
item_details.validate_applied_rule = pricing_rule.get("validate_applied_rule", 0)
item_details.price_or_product_discount = pricing_rule.get("price_or_product_discount")
rules.append(get_pricing_rule_details(args, pricing_rule))
if pricing_rule.mixed_conditions or pricing_rule.apply_rule_on_other:
item_details.update({
'apply_rule_on_other_items': json.dumps(pricing_rule.apply_rule_on_other_items),
'apply_rule_on': (frappe.scrub(pricing_rule.apply_rule_on_other)
if pricing_rule.apply_rule_on_other else frappe.scrub(pricing_rule.get('apply_on')))
})
if pricing_rule.coupon_code_based==1 and args.coupon_code==None:
return item_details
if not pricing_rule.validate_applied_rule:
if pricing_rule.price_or_product_discount == "Price":
apply_price_discount_rule(pricing_rule, item_details, args)
else:
get_product_discount_rule(pricing_rule, item_details, doc)
item_details.has_pricing_rule = 1
item_details.pricing_rules = ','.join([d.pricing_rule for d in rules])
if not doc: return item_details
elif args.get("pricing_rules"):
item_details = remove_pricing_rule_for_item(args.get("pricing_rules"),
item_details, args.get('item_code'))
return item_details
def update_args_for_pricing_rule(args):
if not (args.item_group and args.brand): if not (args.item_group and args.brand):
try: try:
args.item_group, args.brand = frappe.get_cached_value("Item", args.item_code, ["item_group", "brand"]) args.item_group, args.brand = frappe.get_cached_value("Item", args.item_code, ["item_group", "brand"])
@@ -235,57 +290,16 @@ def get_pricing_rule_for_item(args, price_list_rate=0, doc=None):
args.supplier_group = frappe.get_cached_value("Supplier", args.supplier, "supplier_group") args.supplier_group = frappe.get_cached_value("Supplier", args.supplier, "supplier_group")
args.customer = args.customer_group = args.territory = None args.customer = args.customer_group = args.territory = None
pricing_rules = get_pricing_rules(args, doc)
if pricing_rules:
rules = []
for pricing_rule in pricing_rules:
if not pricing_rule or pricing_rule.get('suggestion'): continue
item_details.validate_applied_rule = pricing_rule.get("validate_applied_rule", 0)
rules.append(get_pricing_rule_details(args, pricing_rule))
if pricing_rule.mixed_conditions or pricing_rule.apply_rule_on_other:
item_details.apply_rule_on_other_items = 1
continue
if pricing_rule.coupon_code_based==1 and args.coupon_code==None:
return item_details
if (not pricing_rule.validate_applied_rule and
pricing_rule.price_or_product_discount == "Price"):
apply_price_discount_pricing_rule(pricing_rule, item_details, args)
item_details.has_pricing_rule = 1
# if discount is applied on the rate and not on price list rate
# if price_list_rate:
# set_discount_amount(price_list_rate, item_details)
item_details.pricing_rules = ','.join([d.pricing_rule for d in rules])
if not doc: return item_details
for rule in rules:
doc.append('pricing_rules', rule)
elif args.get("pricing_rules"):
item_details = remove_pricing_rule_for_item(args.get("pricing_rules"),
item_details, args.get('item_code'))
return item_details
def get_pricing_rule_details(args, pricing_rule): def get_pricing_rule_details(args, pricing_rule):
return frappe._dict({ return frappe._dict({
'pricing_rule': pricing_rule.name, 'pricing_rule': pricing_rule.name,
'rate_or_discount': pricing_rule.rate_or_discount, 'rate_or_discount': pricing_rule.rate_or_discount,
'margin_type': pricing_rule.margin_type, 'margin_type': pricing_rule.margin_type,
'item_code': pricing_rule.item_code or args.get("item_code"), 'item_code': args.get("item_code"),
'child_docname': args.get('child_docname') 'child_docname': args.get('child_docname')
}) })
def apply_price_discount_pricing_rule(pricing_rule, item_details, args): def apply_price_discount_rule(pricing_rule, item_details, args):
item_details.pricing_rule_for = pricing_rule.rate_or_discount item_details.pricing_rule_for = pricing_rule.rate_or_discount
if ((pricing_rule.margin_type == 'Amount' and pricing_rule.currency == args.currency) if ((pricing_rule.margin_type == 'Amount' and pricing_rule.currency == args.currency)
@@ -328,10 +342,10 @@ def set_discount_amount(rate, item_details):
item_details.rate = rate item_details.rate = rate
def remove_pricing_rule_for_item(pricing_rules, item_details, item_code=None): def remove_pricing_rule_for_item(pricing_rules, item_details, item_code=None):
from erpnext.accounts.doctype.pricing_rule.utils import get_apply_on_and_items from erpnext.accounts.doctype.pricing_rule.utils import get_pricing_rule_items
for d in pricing_rules.split(','): for d in pricing_rules.split(','):
if not d or not frappe.db.exists("Pricing Rule", d): continue if not d or not frappe.db.exists("Pricing Rule", d): continue
pricing_rule = frappe.get_doc('Pricing Rule', d) pricing_rule = frappe.get_cached_doc('Pricing Rule', d)
if pricing_rule.price_or_product_discount == 'Price': if pricing_rule.price_or_product_discount == 'Price':
if pricing_rule.rate_or_discount == 'Discount Percentage': if pricing_rule.rate_or_discount == 'Discount Percentage':
@@ -349,8 +363,9 @@ def remove_pricing_rule_for_item(pricing_rules, item_details, item_code=None):
else pricing_rule.get('free_item')) else pricing_rule.get('free_item'))
if pricing_rule.get("mixed_conditions") or pricing_rule.get("apply_rule_on_other"): if pricing_rule.get("mixed_conditions") or pricing_rule.get("apply_rule_on_other"):
apply_on, items = get_apply_on_and_items(pricing_rule, item_details) items = get_pricing_rule_items(pricing_rule)
item_details.apply_on = apply_on item_details.apply_on = (frappe.scrub(pricing_rule.apply_rule_on_other)
if pricing_rule.apply_rule_on_other else frappe.scrub(pricing_rule.get('apply_on')))
item_details.applied_on_items = ','.join(items) item_details.applied_on_items = ','.join(items)
item_details.pricing_rules = '' item_details.pricing_rules = ''

View File

@@ -7,7 +7,8 @@ from __future__ import unicode_literals
import frappe, copy, json import frappe, copy, json
from frappe import throw, _ from frappe import throw, _
from six import string_types from six import string_types
from frappe.utils import flt, cint, get_datetime from frappe.utils import flt, cint, get_datetime, get_link_to_form, today
from erpnext.setup.doctype.item_group.item_group import get_child_item_groups
from erpnext.stock.doctype.warehouse.warehouse import get_child_warehouses from erpnext.stock.doctype.warehouse.warehouse import get_child_warehouses
from erpnext.stock.get_item_details import get_conversion_factor from erpnext.stock.get_item_details import get_conversion_factor
@@ -173,10 +174,11 @@ def filter_pricing_rules(args, pricing_rules, doc=None):
if (field and pricing_rules[0].get('other_' + field) != args.get(field)): return if (field and pricing_rules[0].get('other_' + field) != args.get(field)): return
pr_doc = frappe.get_doc('Pricing Rule', pricing_rules[0].name) pr_doc = frappe.get_cached_doc('Pricing Rule', pricing_rules[0].name)
if pricing_rules[0].mixed_conditions and doc: if pricing_rules[0].mixed_conditions and doc:
stock_qty, amount = get_qty_and_rate_for_mixed_conditions(doc, pr_doc, args) stock_qty, amount, items = get_qty_and_rate_for_mixed_conditions(doc, pr_doc, args)
pricing_rules[0].apply_rule_on_other_items = items
elif pricing_rules[0].is_cumulative: elif pricing_rules[0].is_cumulative:
items = [args.get(frappe.scrub(pr_doc.get('apply_on')))] items = [args.get(frappe.scrub(pr_doc.get('apply_on')))]
@@ -282,7 +284,7 @@ def filter_pricing_rules_for_qty_amount(qty, rate, pricing_rules, args=None):
status = True status = True
# if user has created item price against the transaction UOM # if user has created item price against the transaction UOM
if rule.get("uom") == args.get("uom"): if args and rule.get("uom") == args.get("uom"):
conversion_factor = 1.0 conversion_factor = 1.0
if status and (flt(rate) >= (flt(rule.min_amt) * conversion_factor) if status and (flt(rate) >= (flt(rule.min_amt) * conversion_factor)
@@ -339,17 +341,19 @@ def get_qty_and_rate_for_mixed_conditions(doc, pr_doc, args):
sum_qty += data[0] sum_qty += data[0]
sum_amt += data[1] sum_amt += data[1]
return sum_qty, sum_amt return sum_qty, sum_amt, items
def get_qty_and_rate_for_other_item(doc, pr_doc, pricing_rules): def get_qty_and_rate_for_other_item(doc, pr_doc, pricing_rules):
for d in get_pricing_rule_items(pr_doc): items = get_pricing_rule_items(pr_doc)
for row in doc.items:
if d == row.get(frappe.scrub(pr_doc.apply_on)):
pricing_rules = filter_pricing_rules_for_qty_amount(row.get("stock_qty"),
row.get("amount"), pricing_rules, row)
if pricing_rules and pricing_rules[0]: for row in doc.items:
return pricing_rules if row.get(frappe.scrub(pr_doc.apply_rule_on_other)) in items:
pricing_rules = filter_pricing_rules_for_qty_amount(row.get("stock_qty"),
row.get("amount"), pricing_rules, row)
if pricing_rules and pricing_rules[0]:
pricing_rules[0].apply_rule_on_other_items = items
return pricing_rules
def get_qty_amount_data_for_cumulative(pr_doc, doc, items=[]): def get_qty_amount_data_for_cumulative(pr_doc, doc, items=[]):
sum_qty, sum_amt = [0, 0] sum_qty, sum_amt = [0, 0]
@@ -397,39 +401,15 @@ def get_qty_amount_data_for_cumulative(pr_doc, doc, items=[]):
return [sum_qty, sum_amt] return [sum_qty, sum_amt]
def validate_pricing_rules(doc): def apply_pricing_rule_on_transaction(doc):
validate_pricing_rule_on_transactions(doc)
for d in doc.items:
validate_pricing_rule_on_items(doc, d)
doc.calculate_taxes_and_totals()
def validate_pricing_rule_on_items(doc, item_row, do_not_validate = False):
value = 0
for pricing_rule in get_applied_pricing_rules(doc, item_row):
pr_doc = frappe.get_doc('Pricing Rule', pricing_rule)
if (pr_doc.get('apply_on') == 'Transaction'
or not pr_doc.get("mixed_conditions")): continue
if pr_doc.get('price_or_product_discount') == 'Product':
apply_pricing_rule_for_free_items(doc, pr_doc)
else:
for field in ['discount_percentage', 'discount_amount', 'rate']:
if not pr_doc.get(field): continue
value += pr_doc.get(field)
apply_pricing_rule(doc, pr_doc, item_row, value, do_not_validate)
def validate_pricing_rule_on_transactions(doc):
conditions = "apply_on = 'Transaction'" conditions = "apply_on = 'Transaction'"
values = {} values = {}
conditions = get_other_conditions(conditions, values, doc) conditions = get_other_conditions(conditions, values, doc)
pricing_rules = frappe.db.sql(""" Select `tabPricing Rule`.* from `tabPricing Rule` pricing_rules = frappe.db.sql(""" Select `tabPricing Rule`.* from `tabPricing Rule`
where {conditions} """.format(conditions = conditions), values, as_dict=1) where {conditions} and `tabPricing Rule`.disable = 0
""".format(conditions = conditions), values, as_dict=1)
if pricing_rules: if pricing_rules:
pricing_rules = filter_pricing_rules_for_qty_amount(doc.total_qty, pricing_rules = filter_pricing_rules_for_qty_amount(doc.total_qty,
@@ -441,98 +421,83 @@ def validate_pricing_rule_on_transactions(doc):
doc.set('apply_discount_on', d.apply_discount_on) doc.set('apply_discount_on', d.apply_discount_on)
for field in ['additional_discount_percentage', 'discount_amount']: for field in ['additional_discount_percentage', 'discount_amount']:
if not d.get(field): continue
pr_field = ('discount_percentage' pr_field = ('discount_percentage'
if field == 'additional_discount_percentage' else field) if field == 'additional_discount_percentage' else field)
if not d.get(pr_field): continue
if d.validate_applied_rule and doc.get(field) < d.get(pr_field): if d.validate_applied_rule and doc.get(field) < d.get(pr_field):
frappe.msgprint(_("User has not applied rule on the invoice {0}") frappe.msgprint(_("User has not applied rule on the invoice {0}")
.format(doc.name)) .format(doc.name))
else: else:
doc.set(field, d.get(pr_field)) doc.set(field, d.get(pr_field))
elif d.price_or_product_discount == 'Product':
apply_pricing_rule_for_free_items(doc, d)
def get_applied_pricing_rules(doc, item_row): doc.calculate_taxes_and_totals()
elif d.price_or_product_discount == 'Product':
item_details = frappe._dict({'parenttype': doc.doctype})
get_product_discount_rule(d, item_details, doc)
apply_pricing_rule_for_free_items(doc, item_details.free_item_data)
doc.set_missing_values()
def get_applied_pricing_rules(item_row):
return (item_row.get("pricing_rules").split(',') return (item_row.get("pricing_rules").split(',')
if item_row.get("pricing_rules") else []) if item_row.get("pricing_rules") else [])
def apply_pricing_rule_for_free_items(doc, pricing_rule): def get_product_discount_rule(pricing_rule, item_details, doc=None):
if pricing_rule.get('free_item'): free_item = (pricing_rule.free_item
if not pricing_rule.same_item or pricing_rule.apply_on == 'Transaction' else item_details.item_code)
if not free_item:
frappe.throw(_("Free item not set in the pricing rule {0}")
.format(get_link_to_form("Pricing Rule", pricing_rule.name)))
item_details.free_item_data = {
'item_code': free_item,
'qty': pricing_rule.free_qty or 1,
'rate': pricing_rule.free_item_rate or 0,
'price_list_rate': pricing_rule.free_item_rate or 0,
'is_free_item': 1
}
item_data = frappe.get_cached_value('Item', free_item, ['item_name',
'description', 'stock_uom'], as_dict=1)
item_details.free_item_data.update(item_data)
item_details.free_item_data['uom'] = pricing_rule.free_item_uom or item_data.stock_uom
item_details.free_item_data['conversion_factor'] = get_conversion_factor(free_item,
item_details.free_item_data['uom']).get("conversion_factor", 1)
if item_details.get("parenttype") == 'Purchase Order':
item_details.free_item_data['schedule_date'] = doc.schedule_date if doc else today()
if item_details.get("parenttype") == 'Sales Order':
item_details.free_item_data['delivery_date'] = doc.delivery_date if doc else today()
def apply_pricing_rule_for_free_items(doc, pricing_rule_args, set_missing_values=False):
if pricing_rule_args.get('item_code'):
items = [d.item_code for d in doc.items items = [d.item_code for d in doc.items
if d.item_code == (d.item_code if d.item_code == (pricing_rule_args.get("item_code")) and d.is_free_item]
if pricing_rule.get('same_item') else pricing_rule.get('free_item')) and d.is_free_item]
if not items: if not items:
doc.append('items', { doc.append('items', pricing_rule_args)
'item_code': pricing_rule.get('free_item'),
'qty': pricing_rule.get('free_qty'),
'uom': pricing_rule.get('free_item_uom'),
'rate': pricing_rule.get('free_item_rate'),
'is_free_item': 1
})
doc.set_missing_values()
def apply_pricing_rule(doc, pr_doc, item_row, value, do_not_validate=False):
apply_on, items = get_apply_on_and_items(pr_doc, item_row)
rule_applied = {}
for item in doc.get("items"):
if item.get(apply_on) in items:
if not item.pricing_rules:
item.pricing_rules = item_row.pricing_rules
for field in ['discount_percentage', 'discount_amount', 'rate']:
if not pr_doc.get(field): continue
key = (item.name, item.pricing_rules)
if not pr_doc.validate_applied_rule:
rule_applied[key] = 1
item.set(field, value)
elif item.get(field) < value:
if not do_not_validate and item.idx == item_row.idx:
rule_applied[key] = 0
frappe.msgprint(_("Row {0}: user has not applied rule <b>{1}</b> on the item <b>{2}</b>")
.format(item.idx, pr_doc.title, item.item_code))
if rule_applied and doc.get("pricing_rules"):
for d in doc.get("pricing_rules"):
key = (d.child_docname, d.pricing_rule)
if key in rule_applied:
d.rule_applied = 1
def get_apply_on_and_items(pr_doc, item_row):
# for mixed or other items conditions
apply_on = frappe.scrub(pr_doc.get('apply_on'))
items = (get_pricing_rule_items(pr_doc)
if pr_doc.mixed_conditions else [item_row.get(apply_on)])
if pr_doc.apply_rule_on_other:
apply_on = frappe.scrub(pr_doc.apply_rule_on_other)
items = [pr_doc.get(apply_on)]
return apply_on, items
def get_pricing_rule_items(pr_doc): def get_pricing_rule_items(pr_doc):
apply_on_data = []
apply_on = frappe.scrub(pr_doc.get('apply_on')) apply_on = frappe.scrub(pr_doc.get('apply_on'))
pricing_rule_apply_on = apply_on_table.get(pr_doc.get('apply_on')) pricing_rule_apply_on = apply_on_table.get(pr_doc.get('apply_on'))
return [item.get(apply_on) for item in pr_doc.get(pricing_rule_apply_on)] or [] for d in pr_doc.get(pricing_rule_apply_on):
if apply_on == 'item_group':
get_child_item_groups(d.get(apply_on))
else:
apply_on_data.append(d.get(apply_on))
@frappe.whitelist() if pr_doc.apply_rule_on_other:
def validate_pricing_rule_for_different_cond(doc): apply_on = frappe.scrub(pr_doc.apply_rule_on_other)
if isinstance(doc, string_types): apply_on_data.append(pr_doc.get(apply_on))
doc = json.loads(doc)
doc = frappe.get_doc(doc) return list(set(apply_on_data))
for d in doc.get("items"):
validate_pricing_rule_on_items(doc, d, True)
return doc
def validate_coupon_code(coupon_name): def validate_coupon_code(coupon_name):
from frappe.utils import today,getdate from frappe.utils import today,getdate

View File

@@ -248,7 +248,7 @@ class PurchaseInvoice(BuyingController):
def set_against_expense_account(self): def set_against_expense_account(self):
against_accounts = [] against_accounts = []
for item in self.get("items"): for item in self.get("items"):
if item.expense_account not in against_accounts: if item.expense_account and (item.expense_account not in against_accounts):
against_accounts.append(item.expense_account) against_accounts.append(item.expense_account)
self.against_expense_account = ",".join(against_accounts) self.against_expense_account = ",".join(against_accounts)

View File

@@ -0,0 +1,3 @@
{% include "erpnext/regional/india/taxes.js" %}
erpnext.setup_auto_gst_taxation('Purchase Invoice');

View File

@@ -1,4 +1,5 @@
{ {
"actions": [],
"autoname": "hash", "autoname": "hash",
"creation": "2013-05-22 12:43:10", "creation": "2013-05-22 12:43:10",
"doctype": "DocType", "doctype": "DocType",
@@ -507,7 +508,8 @@
"depends_on": "enable_deferred_expense", "depends_on": "enable_deferred_expense",
"fieldname": "service_stop_date", "fieldname": "service_stop_date",
"fieldtype": "Date", "fieldtype": "Date",
"label": "Service Stop Date" "label": "Service Stop Date",
"no_copy": 1
}, },
{ {
"default": "0", "default": "0",
@@ -523,13 +525,15 @@
"depends_on": "enable_deferred_expense", "depends_on": "enable_deferred_expense",
"fieldname": "service_start_date", "fieldname": "service_start_date",
"fieldtype": "Date", "fieldtype": "Date",
"label": "Service Start Date" "label": "Service Start Date",
"no_copy": 1
}, },
{ {
"depends_on": "enable_deferred_expense", "depends_on": "enable_deferred_expense",
"fieldname": "service_end_date", "fieldname": "service_end_date",
"fieldtype": "Date", "fieldtype": "Date",
"label": "Service End Date" "label": "Service End Date",
"no_copy": 1
}, },
{ {
"fieldname": "reference", "fieldname": "reference",
@@ -766,7 +770,8 @@
], ],
"idx": 1, "idx": 1,
"istable": 1, "istable": 1,
"modified": "2019-11-21 16:27:52.043744", "links": [],
"modified": "2019-12-04 12:23:17.046413",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Purchase Invoice Item", "name": "Purchase Invoice Item",

View File

@@ -1,300 +1,108 @@
{ {
"allow_copy": 0, "allow_import": 1,
"allow_import": 1, "allow_rename": 1,
"allow_rename": 1, "creation": "2013-01-10 16:34:08",
"autoname": "field:title", "description": "Standard tax template that can be applied to all Purchase Transactions. This template can contain list of tax heads and also other expense heads like \"Shipping\", \"Insurance\", \"Handling\" etc.\n\n#### Note\n\nThe tax rate you define here will be the standard tax rate for all **Items**. If there are **Items** that have different rates, they must be added in the **Item Tax** table in the **Item** master.\n\n#### Description of Columns\n\n1. Calculation Type: \n - This can be on **Net Total** (that is the sum of basic amount).\n - **On Previous Row Total / Amount** (for cumulative taxes or charges). If you select this option, the tax will be applied as a percentage of the previous row (in the tax table) amount or total.\n - **Actual** (as mentioned).\n2. Account Head: The Account ledger under which this tax will be booked\n3. Cost Center: If the tax / charge is an income (like shipping) or expense it needs to be booked against a Cost Center.\n4. Description: Description of the tax (that will be printed in invoices / quotes).\n5. Rate: Tax rate.\n6. Amount: Tax amount.\n7. Total: Cumulative total to this point.\n8. Enter Row: If based on \"Previous Row Total\" you can select the row number which will be taken as a base for this calculation (default is the previous row).\n9. Consider Tax or Charge for: In this section you can specify if the tax / charge is only for valuation (not a part of total) or only for total (does not add value to the item) or for both.\n10. Add or Deduct: Whether you want to add or deduct the tax.",
"beta": 0, "doctype": "DocType",
"creation": "2013-01-10 16:34:08", "document_type": "Setup",
"custom": 0, "field_order": [
"description": "Standard tax template that can be applied to all Purchase Transactions. This template can contain list of tax heads and also other expense heads like \"Shipping\", \"Insurance\", \"Handling\" etc.\n\n#### Note\n\nThe tax rate you define here will be the standard tax rate for all **Items**. If there are **Items** that have different rates, they must be added in the **Item Tax** table in the **Item** master.\n\n#### Description of Columns\n\n1. Calculation Type: \n - This can be on **Net Total** (that is the sum of basic amount).\n - **On Previous Row Total / Amount** (for cumulative taxes or charges). If you select this option, the tax will be applied as a percentage of the previous row (in the tax table) amount or total.\n - **Actual** (as mentioned).\n2. Account Head: The Account ledger under which this tax will be booked\n3. Cost Center: If the tax / charge is an income (like shipping) or expense it needs to be booked against a Cost Center.\n4. Description: Description of the tax (that will be printed in invoices / quotes).\n5. Rate: Tax rate.\n6. Amount: Tax amount.\n7. Total: Cumulative total to this point.\n8. Enter Row: If based on \"Previous Row Total\" you can select the row number which will be taken as a base for this calculation (default is the previous row).\n9. Consider Tax or Charge for: In this section you can specify if the tax / charge is only for valuation (not a part of total) or only for total (does not add value to the item) or for both.\n10. Add or Deduct: Whether you want to add or deduct the tax.", "title",
"docstatus": 0, "is_default",
"doctype": "DocType", "disabled",
"document_type": "Setup", "column_break4",
"editable_grid": 0, "company",
"tax_category",
"section_break6",
"taxes"
],
"fields": [ "fields": [
{ {
"allow_on_submit": 0, "fieldname": "title",
"bold": 0, "fieldtype": "Data",
"collapsible": 0, "label": "Title",
"columns": 0, "no_copy": 1,
"fieldname": "title", "oldfieldname": "title",
"fieldtype": "Data", "oldfieldtype": "Data",
"hidden": 0, "reqd": 1
"ignore_user_permissions": 0, },
"ignore_xss_filter": 0,
"in_filter": 1,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Title",
"length": 0,
"no_copy": 1,
"oldfieldname": "title",
"oldfieldtype": "Data",
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{ {
"allow_on_submit": 0, "default": "0",
"bold": 0, "fieldname": "is_default",
"collapsible": 0, "fieldtype": "Check",
"columns": 0, "in_list_view": 1,
"fieldname": "is_default", "label": "Default"
"fieldtype": "Check", },
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Default",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{ {
"allow_on_submit": 0, "default": "0",
"bold": 0, "fieldname": "disabled",
"collapsible": 0, "fieldtype": "Check",
"columns": 0, "in_list_view": 1,
"fieldname": "disabled", "label": "Disabled"
"fieldtype": "Check", },
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Disabled",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{ {
"allow_on_submit": 0, "fieldname": "column_break4",
"bold": 0, "fieldtype": "Column Break"
"collapsible": 0, },
"columns": 0,
"fieldname": "column_break4",
"fieldtype": "Column Break",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{ {
"allow_on_submit": 0, "fieldname": "company",
"bold": 0, "fieldtype": "Link",
"collapsible": 0, "in_list_view": 1,
"columns": 0, "in_standard_filter": 1,
"fieldname": "company", "label": "Company",
"fieldtype": "Link", "options": "Company",
"hidden": 0, "remember_last_selected_value": 1,
"ignore_user_permissions": 0, "reqd": 1
"ignore_xss_filter": 0, },
"in_filter": 1,
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Company",
"length": 0,
"no_copy": 0,
"options": "Company",
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 1,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{ {
"allow_on_submit": 0, "fieldname": "section_break6",
"bold": 0, "fieldtype": "Section Break"
"collapsible": 0, },
"columns": 0,
"fieldname": "section_break6",
"fieldtype": "Section Break",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{ {
"allow_on_submit": 0, "fieldname": "taxes",
"bold": 0, "fieldtype": "Table",
"collapsible": 0, "label": "Purchase Taxes and Charges",
"columns": 0, "oldfieldname": "purchase_tax_details",
"fieldname": "taxes", "oldfieldtype": "Table",
"fieldtype": "Table", "options": "Purchase Taxes and Charges"
"hidden": 0, },
"ignore_user_permissions": 0, {
"ignore_xss_filter": 0, "fieldname": "tax_category",
"in_filter": 0, "fieldtype": "Link",
"in_list_view": 0, "label": "Tax Category",
"in_standard_filter": 0, "options": "Tax Category"
"label": "Purchase Taxes and Charges",
"length": 0,
"no_copy": 0,
"oldfieldname": "purchase_tax_details",
"oldfieldtype": "Table",
"options": "Purchase Taxes and Charges",
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
} }
], ],
"hide_heading": 0, "icon": "fa fa-money",
"hide_toolbar": 0, "idx": 1,
"icon": "fa fa-money", "modified": "2019-11-25 13:05:26.220275",
"idx": 1, "modified_by": "Administrator",
"image_view": 0, "module": "Accounts",
"in_create": 0, "name": "Purchase Taxes and Charges Template",
"owner": "wasim@webnotestech.com",
"is_submittable": 0,
"issingle": 0,
"istable": 0,
"max_attachments": 0,
"modified": "2016-11-07 05:18:44.095798",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Purchase Taxes and Charges Template",
"owner": "wasim@webnotestech.com",
"permissions": [ "permissions": [
{ {
"amend": 0, "email": 1,
"apply_user_permissions": 0, "print": 1,
"cancel": 0, "read": 1,
"create": 0, "report": 1,
"delete": 0, "role": "Purchase Manager"
"email": 1, },
"export": 0,
"if_owner": 0,
"import": 0,
"is_custom": 0,
"permlevel": 0,
"print": 1,
"read": 1,
"report": 1,
"role": "Purchase Manager",
"set_user_permissions": 0,
"share": 0,
"submit": 0,
"write": 0
},
{ {
"amend": 0, "create": 1,
"apply_user_permissions": 0, "delete": 1,
"cancel": 0, "email": 1,
"create": 1, "print": 1,
"delete": 1, "read": 1,
"email": 1, "report": 1,
"export": 0, "role": "Purchase Master Manager",
"if_owner": 0, "share": 1,
"import": 0,
"is_custom": 0,
"permlevel": 0,
"print": 1,
"read": 1,
"report": 1,
"role": "Purchase Master Manager",
"set_user_permissions": 0,
"share": 1,
"submit": 0,
"write": 1 "write": 1
}, },
{ {
"amend": 0, "read": 1,
"apply_user_permissions": 0, "role": "Purchase User"
"cancel": 0,
"create": 0,
"delete": 0,
"email": 0,
"export": 0,
"if_owner": 0,
"import": 0,
"is_custom": 0,
"permlevel": 0,
"print": 0,
"read": 1,
"report": 0,
"role": "Purchase User",
"set_user_permissions": 0,
"share": 0,
"submit": 0,
"write": 0
} }
], ],
"quick_entry": 0, "sort_order": "DESC",
"read_only": 0, "track_changes": 1
"read_only_onload": 0,
"sort_order": "DESC",
"track_seen": 0
} }

View File

@@ -357,14 +357,11 @@ def get_customer_wise_price_list():
def get_bin_data(pos_profile): def get_bin_data(pos_profile):
itemwise_bin_data = {} itemwise_bin_data = {}
cond = "1=1" filters = { 'actual_qty': ['>', 0] }
if pos_profile.get('warehouse'): if pos_profile.get('warehouse'):
cond = "warehouse = %(warehouse)s" filters.update({ 'warehouse': pos_profile.get('warehouse') })
bin_data = frappe.db.sql(""" select item_code, warehouse, actual_qty from `tabBin` bin_data = frappe.db.get_all('Bin', fields = ['item_code', 'warehouse', 'actual_qty'], filters=filters)
where actual_qty > 0 and {cond}""".format(cond=cond), {
'warehouse': frappe.db.escape(pos_profile.get('warehouse'))
}, as_dict=1)
for bins in bin_data: for bins in bin_data:
if bins.item_code not in itemwise_bin_data: if bins.item_code not in itemwise_bin_data:
@@ -550,11 +547,15 @@ def make_address(args, customer):
def make_email_queue(email_queue): def make_email_queue(email_queue):
name_list = [] name_list = []
for key, data in iteritems(email_queue): for key, data in iteritems(email_queue):
name = frappe.db.get_value('Sales Invoice', {'offline_pos_name': key}, 'name') name = frappe.db.get_value('Sales Invoice', {'offline_pos_name': key}, 'name')
if not name: continue
data = json.loads(data) data = json.loads(data)
sender = frappe.session.user sender = frappe.session.user
print_format = "POS Invoice" if not cint(frappe.db.get_value('Print Format', 'POS Invoice', 'disabled')) else None print_format = "POS Invoice" if not cint(frappe.db.get_value('Print Format', 'POS Invoice', 'disabled')) else None
attachments = [frappe.attach_print('Sales Invoice', name, print_format=print_format)] attachments = [frappe.attach_print('Sales Invoice', name, print_format=print_format)]
make(subject=data.get('subject'), content=data.get('content'), recipients=data.get('recipients'), make(subject=data.get('subject'), content=data.get('content'), recipients=data.get('recipients'),

View File

@@ -1,3 +1,7 @@
{% include "erpnext/regional/india/taxes.js" %}
erpnext.setup_auto_gst_taxation('Sales Invoice');
frappe.ui.form.on("Sales Invoice", { frappe.ui.form.on("Sales Invoice", {
setup: function(frm) { setup: function(frm) {
frm.set_query('transporter', function() { frm.set_query('transporter', function() {
@@ -34,5 +38,8 @@ frappe.ui.form.on("Sales Invoice", {
} }
}, __("Make")); }, __("Make"));
} }
} },
}); });

View File

@@ -697,8 +697,8 @@ frappe.ui.form.on('Sales Invoice', {
if (frm.doc.company) if (frm.doc.company)
{ {
frappe.call({ frappe.call({
method:"frappe.contacts.doctype.address.address.get_default_address", method:"erpnext.setup.doctype.company.company.get_default_company_address",
args:{ doctype:'Company',name:frm.doc.company}, args:{name:frm.doc.company, existing_address: frm.doc.company_address},
callback: function(r){ callback: function(r){
if (r.message){ if (r.message){
frm.set_value("company_address",r.message) frm.set_value("company_address",r.message)

View File

@@ -1,4 +1,5 @@
{ {
"actions": [],
"autoname": "hash", "autoname": "hash",
"creation": "2013-06-04 11:02:19", "creation": "2013-06-04 11:02:19",
"doctype": "DocType", "doctype": "DocType",
@@ -484,7 +485,8 @@
"depends_on": "enable_deferred_revenue", "depends_on": "enable_deferred_revenue",
"fieldname": "service_stop_date", "fieldname": "service_stop_date",
"fieldtype": "Date", "fieldtype": "Date",
"label": "Service Stop Date" "label": "Service Stop Date",
"no_copy": 1
}, },
{ {
"default": "0", "default": "0",
@@ -500,13 +502,15 @@
"depends_on": "enable_deferred_revenue", "depends_on": "enable_deferred_revenue",
"fieldname": "service_start_date", "fieldname": "service_start_date",
"fieldtype": "Date", "fieldtype": "Date",
"label": "Service Start Date" "label": "Service Start Date",
"no_copy": 1
}, },
{ {
"depends_on": "enable_deferred_revenue", "depends_on": "enable_deferred_revenue",
"fieldname": "service_end_date", "fieldname": "service_end_date",
"fieldtype": "Date", "fieldtype": "Date",
"label": "Service End Date" "label": "Service End Date",
"no_copy": 1
}, },
{ {
"collapsible": 1, "collapsible": 1,
@@ -783,7 +787,8 @@
], ],
"idx": 1, "idx": 1,
"istable": 1, "istable": 1,
"modified": "2019-07-16 16:36:46.527606", "links": [],
"modified": "2019-12-04 12:22:38.517710",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Sales Invoice Item", "name": "Sales Invoice Item",

View File

@@ -1,299 +1,119 @@
{ {
"allow_copy": 0, "allow_import": 1,
"allow_import": 1, "allow_rename": 1,
"allow_rename": 1, "creation": "2013-01-10 16:34:09",
"autoname": "field:title", "description": "Standard tax template that can be applied to all Sales Transactions. This template can contain list of tax heads and also other expense / income heads like \"Shipping\", \"Insurance\", \"Handling\" etc.\n\n#### Note\n\nThe tax rate you define here will be the standard tax rate for all **Items**. If there are **Items** that have different rates, they must be added in the **Item Tax** table in the **Item** master.\n\n#### Description of Columns\n\n1. Calculation Type: \n - This can be on **Net Total** (that is the sum of basic amount).\n - **On Previous Row Total / Amount** (for cumulative taxes or charges). If you select this option, the tax will be applied as a percentage of the previous row (in the tax table) amount or total.\n - **Actual** (as mentioned).\n2. Account Head: The Account ledger under which this tax will be booked\n3. Cost Center: If the tax / charge is an income (like shipping) or expense it needs to be booked against a Cost Center.\n4. Description: Description of the tax (that will be printed in invoices / quotes).\n5. Rate: Tax rate.\n6. Amount: Tax amount.\n7. Total: Cumulative total to this point.\n8. Enter Row: If based on \"Previous Row Total\" you can select the row number which will be taken as a base for this calculation (default is the previous row).\n9. Is this Tax included in Basic Rate?: If you check this, it means that this tax will not be shown below the item table, but will be included in the Basic Rate in your main item table. This is useful where you want give a flat price (inclusive of all taxes) price to customers.",
"beta": 0, "doctype": "DocType",
"creation": "2013-01-10 16:34:09", "document_type": "Setup",
"custom": 0, "engine": "InnoDB",
"description": "Standard tax template that can be applied to all Sales Transactions. This template can contain list of tax heads and also other expense / income heads like \"Shipping\", \"Insurance\", \"Handling\" etc.\n\n#### Note\n\nThe tax rate you define here will be the standard tax rate for all **Items**. If there are **Items** that have different rates, they must be added in the **Item Tax** table in the **Item** master.\n\n#### Description of Columns\n\n1. Calculation Type: \n - This can be on **Net Total** (that is the sum of basic amount).\n - **On Previous Row Total / Amount** (for cumulative taxes or charges). If you select this option, the tax will be applied as a percentage of the previous row (in the tax table) amount or total.\n - **Actual** (as mentioned).\n2. Account Head: The Account ledger under which this tax will be booked\n3. Cost Center: If the tax / charge is an income (like shipping) or expense it needs to be booked against a Cost Center.\n4. Description: Description of the tax (that will be printed in invoices / quotes).\n5. Rate: Tax rate.\n6. Amount: Tax amount.\n7. Total: Cumulative total to this point.\n8. Enter Row: If based on \"Previous Row Total\" you can select the row number which will be taken as a base for this calculation (default is the previous row).\n9. Is this Tax included in Basic Rate?: If you check this, it means that this tax will not be shown below the item table, but will be included in the Basic Rate in your main item table. This is useful where you want give a flat price (inclusive of all taxes) price to customers.", "field_order": [
"docstatus": 0, "title",
"doctype": "DocType", "is_default",
"document_type": "Setup", "disabled",
"editable_grid": 0, "column_break_3",
"company",
"tax_category",
"section_break_5",
"taxes"
],
"fields": [ "fields": [
{ {
"allow_on_submit": 0, "fieldname": "title",
"bold": 0, "fieldtype": "Data",
"collapsible": 0, "label": "Title",
"columns": 0, "no_copy": 1,
"fieldname": "title", "oldfieldname": "title",
"fieldtype": "Data", "oldfieldtype": "Data",
"hidden": 0, "reqd": 1
"ignore_user_permissions": 0, },
"ignore_xss_filter": 0,
"in_filter": 1,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Title",
"length": 0,
"no_copy": 1,
"oldfieldname": "title",
"oldfieldtype": "Data",
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{ {
"allow_on_submit": 0, "default": "0",
"bold": 0, "fieldname": "is_default",
"collapsible": 0, "fieldtype": "Check",
"columns": 0, "in_list_view": 1,
"fieldname": "is_default", "label": "Default"
"fieldtype": "Check", },
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Default",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{ {
"allow_on_submit": 0, "default": "0",
"bold": 0, "fieldname": "disabled",
"collapsible": 0, "fieldtype": "Check",
"columns": 0, "label": "Disabled"
"fieldname": "disabled", },
"fieldtype": "Check",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Disabled",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{ {
"allow_on_submit": 0, "fieldname": "column_break_3",
"bold": 0, "fieldtype": "Column Break"
"collapsible": 0, },
"columns": 0,
"fieldname": "column_break_3",
"fieldtype": "Column Break",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"length": 0,
"no_copy": 0,
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{ {
"allow_on_submit": 0, "fieldname": "company",
"bold": 0, "fieldtype": "Link",
"collapsible": 0, "in_list_view": 1,
"columns": 0, "in_standard_filter": 1,
"fieldname": "company", "label": "Company",
"fieldtype": "Link", "oldfieldname": "company",
"hidden": 0, "oldfieldtype": "Link",
"ignore_user_permissions": 0, "options": "Company",
"ignore_xss_filter": 0, "remember_last_selected_value": 1,
"in_filter": 1, "reqd": 1
"in_list_view": 1, },
"in_standard_filter": 1,
"label": "Company",
"length": 0,
"no_copy": 0,
"oldfieldname": "company",
"oldfieldtype": "Link",
"options": "Company",
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 1,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{ {
"allow_on_submit": 0, "fieldname": "section_break_5",
"bold": 0, "fieldtype": "Section Break"
"collapsible": 0, },
"columns": 0,
"fieldname": "section_break_5",
"fieldtype": "Section Break",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"length": 0,
"no_copy": 0,
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{ {
"allow_on_submit": 0, "description": "* Will be calculated in the transaction.",
"bold": 0, "fieldname": "taxes",
"collapsible": 0, "fieldtype": "Table",
"columns": 0, "label": "Sales Taxes and Charges",
"description": "* Will be calculated in the transaction.", "oldfieldname": "other_charges",
"fieldname": "taxes", "oldfieldtype": "Table",
"fieldtype": "Table", "options": "Sales Taxes and Charges"
"hidden": 0, },
"ignore_user_permissions": 0, {
"ignore_xss_filter": 0, "fieldname": "tax_category",
"in_filter": 0, "fieldtype": "Link",
"in_list_view": 0, "label": "Tax Category",
"in_standard_filter": 0, "options": "Tax Category"
"label": "Sales Taxes and Charges",
"length": 0,
"no_copy": 0,
"oldfieldname": "other_charges",
"oldfieldtype": "Table",
"options": "Sales Taxes and Charges",
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
} }
], ],
"hide_heading": 0, "icon": "fa fa-money",
"hide_toolbar": 0, "idx": 1,
"icon": "fa fa-money", "modified": "2019-11-25 13:06:03.279099",
"idx": 1, "modified_by": "Administrator",
"image_view": 0, "module": "Accounts",
"in_create": 0, "name": "Sales Taxes and Charges Template",
"owner": "Administrator",
"is_submittable": 0,
"issingle": 0,
"istable": 0,
"max_attachments": 0,
"modified": "2016-11-07 05:18:41.743257",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Sales Taxes and Charges Template",
"owner": "Administrator",
"permissions": [ "permissions": [
{ {
"amend": 0, "email": 1,
"apply_user_permissions": 1, "print": 1,
"cancel": 0, "read": 1,
"create": 0, "report": 1,
"delete": 0, "role": "Sales User"
"email": 1, },
"export": 0,
"if_owner": 0,
"import": 0,
"is_custom": 0,
"permlevel": 0,
"print": 1,
"read": 1,
"report": 1,
"role": "Sales User",
"set_user_permissions": 0,
"share": 0,
"submit": 0,
"write": 0
},
{ {
"amend": 0, "create": 1,
"apply_user_permissions": 0, "delete": 1,
"cancel": 0, "email": 1,
"create": 1, "print": 1,
"delete": 1, "read": 1,
"email": 1, "report": 1,
"export": 0, "role": "Accounts Manager",
"if_owner": 0, "share": 1,
"import": 0,
"is_custom": 0,
"permlevel": 0,
"print": 1,
"read": 1,
"report": 1,
"role": "Accounts Manager",
"set_user_permissions": 0,
"share": 1,
"submit": 0,
"write": 1 "write": 1
}, },
{ {
"amend": 0, "create": 1,
"apply_user_permissions": 0, "delete": 1,
"cancel": 0, "email": 1,
"create": 1, "print": 1,
"delete": 1, "read": 1,
"email": 1, "report": 1,
"export": 0, "role": "Sales Master Manager",
"if_owner": 0, "share": 1,
"import": 0,
"is_custom": 0,
"permlevel": 0,
"print": 1,
"read": 1,
"report": 1,
"role": "Sales Master Manager",
"set_user_permissions": 0,
"share": 1,
"submit": 0,
"write": 1 "write": 1
} }
], ],
"quick_entry": 0, "sort_field": "modified",
"read_only": 0, "sort_order": "ASC",
"read_only_onload": 0, "track_changes": 1
"sort_order": "ASC",
"track_seen": 0
} }

View File

@@ -13,9 +13,9 @@ from frappe.utils import nowdate
class ShareDontExists(ValidationError): pass class ShareDontExists(ValidationError): pass
class ShareTransfer(Document): class ShareTransfer(Document):
def before_submit(self): def on_submit(self):
if self.transfer_type == 'Issue': if self.transfer_type == 'Issue':
shareholder = self.get_shareholder_doc(self.company) shareholder = self.get_company_shareholder()
shareholder.append('share_balance', { shareholder.append('share_balance', {
'share_type': self.share_type, 'share_type': self.share_type,
'from_no': self.from_no, 'from_no': self.from_no,
@@ -28,7 +28,7 @@ class ShareTransfer(Document):
}) })
shareholder.save() shareholder.save()
doc = frappe.get_doc('Shareholder', self.to_shareholder) doc = self.get_shareholder_doc(self.to_shareholder)
doc.append('share_balance', { doc.append('share_balance', {
'share_type': self.share_type, 'share_type': self.share_type,
'from_no': self.from_no, 'from_no': self.from_no,
@@ -41,11 +41,11 @@ class ShareTransfer(Document):
elif self.transfer_type == 'Purchase': elif self.transfer_type == 'Purchase':
self.remove_shares(self.from_shareholder) self.remove_shares(self.from_shareholder)
self.remove_shares(self.get_shareholder_doc(self.company).name) self.remove_shares(self.get_company_shareholder().name)
elif self.transfer_type == 'Transfer': elif self.transfer_type == 'Transfer':
self.remove_shares(self.from_shareholder) self.remove_shares(self.from_shareholder)
doc = frappe.get_doc('Shareholder', self.to_shareholder) doc = self.get_shareholder_doc(self.to_shareholder)
doc.append('share_balance', { doc.append('share_balance', {
'share_type': self.share_type, 'share_type': self.share_type,
'from_no': self.from_no, 'from_no': self.from_no,
@@ -56,143 +56,127 @@ class ShareTransfer(Document):
}) })
doc.save() doc.save()
def on_cancel(self):
if self.transfer_type == 'Issue':
compnay_shareholder = self.get_company_shareholder()
self.remove_shares(compnay_shareholder.name)
self.remove_shares(self.to_shareholder)
elif self.transfer_type == 'Purchase':
compnay_shareholder = self.get_company_shareholder()
from_shareholder = self.get_shareholder_doc(self.from_shareholder)
from_shareholder.append('share_balance', {
'share_type': self.share_type,
'from_no': self.from_no,
'to_no': self.to_no,
'rate': self.rate,
'amount': self.amount,
'no_of_shares': self.no_of_shares
})
from_shareholder.save()
compnay_shareholder.append('share_balance', {
'share_type': self.share_type,
'from_no': self.from_no,
'to_no': self.to_no,
'rate': self.rate,
'amount': self.amount,
'no_of_shares': self.no_of_shares
})
compnay_shareholder.save()
elif self.transfer_type == 'Transfer':
self.remove_shares(self.to_shareholder)
from_shareholder = self.get_shareholder_doc(self.from_shareholder)
from_shareholder.append('share_balance', {
'share_type': self.share_type,
'from_no': self.from_no,
'to_no': self.to_no,
'rate': self.rate,
'amount': self.amount,
'no_of_shares': self.no_of_shares
})
from_shareholder.save()
def validate(self): def validate(self):
self.get_company_shareholder()
self.basic_validations() self.basic_validations()
self.folio_no_validation() self.folio_no_validation()
if self.transfer_type == 'Issue': if self.transfer_type == 'Issue':
if not self.get_shareholder_doc(self.company): # validate share doesn't exist in company
shareholder = frappe.get_doc({ ret_val = self.share_exists(self.get_company_shareholder().name)
'doctype': 'Shareholder', if ret_val in ('Complete', 'Partial'):
'title': self.company,
'company': self.company,
'is_company': 1
})
shareholder.insert()
# validate share doesnt exist in company
ret_val = self.share_exists(self.get_shareholder_doc(self.company).name)
if ret_val != False:
frappe.throw(_('The shares already exist'), frappe.DuplicateEntryError) frappe.throw(_('The shares already exist'), frappe.DuplicateEntryError)
else: else:
# validate share exists with from_shareholder # validate share exists with from_shareholder
ret_val = self.share_exists(self.from_shareholder) ret_val = self.share_exists(self.from_shareholder)
if ret_val != True: if ret_val in ('Outside', 'Partial'):
frappe.throw(_("The shares don't exist with the {0}") frappe.throw(_("The shares don't exist with the {0}")
.format(self.from_shareholder), ShareDontExists) .format(self.from_shareholder), ShareDontExists)
def basic_validations(self): def basic_validations(self):
if self.transfer_type == 'Purchase': if self.transfer_type == 'Purchase':
self.to_shareholder = '' self.to_shareholder = ''
if self.from_shareholder is None or self.from_shareholder is '': if not self.from_shareholder:
frappe.throw(_('The field From Shareholder cannot be blank')) frappe.throw(_('The field From Shareholder cannot be blank'))
if self.from_folio_no is None or self.from_folio_no is '': if not self.from_folio_no:
self.to_folio_no = self.autoname_folio(self.to_shareholder) self.to_folio_no = self.autoname_folio(self.to_shareholder)
if self.asset_account is None: if not self.asset_account:
frappe.throw(_('The field Asset Account cannot be blank')) frappe.throw(_('The field Asset Account cannot be blank'))
elif (self.transfer_type == 'Issue'): elif (self.transfer_type == 'Issue'):
self.from_shareholder = '' self.from_shareholder = ''
if self.to_shareholder is None or self.to_shareholder == '': if not self.to_shareholder:
frappe.throw(_('The field To Shareholder cannot be blank')) frappe.throw(_('The field To Shareholder cannot be blank'))
if self.to_folio_no is None or self.to_folio_no is '': if not self.to_folio_no:
self.to_folio_no = self.autoname_folio(self.to_shareholder) self.to_folio_no = self.autoname_folio(self.to_shareholder)
if self.asset_account is None: if not self.asset_account:
frappe.throw(_('The field Asset Account cannot be blank')) frappe.throw(_('The field Asset Account cannot be blank'))
else: else:
if self.from_shareholder is None or self.to_shareholder is None: if not self.from_shareholder or not self.to_shareholder:
frappe.throw(_('The fields From Shareholder and To Shareholder cannot be blank')) frappe.throw(_('The fields From Shareholder and To Shareholder cannot be blank'))
if self.to_folio_no is None or self.to_folio_no is '': if not self.to_folio_no:
self.to_folio_no = self.autoname_folio(self.to_shareholder) self.to_folio_no = self.autoname_folio(self.to_shareholder)
if self.equity_or_liability_account is None: if not self.equity_or_liability_account:
frappe.throw(_('The field Equity/Liability Account cannot be blank')) frappe.throw(_('The field Equity/Liability Account cannot be blank'))
if self.from_shareholder == self.to_shareholder: if self.from_shareholder == self.to_shareholder:
frappe.throw(_('The seller and the buyer cannot be the same')) frappe.throw(_('The seller and the buyer cannot be the same'))
if self.no_of_shares != self.to_no - self.from_no + 1: if self.no_of_shares != self.to_no - self.from_no + 1:
frappe.throw(_('The number of shares and the share numbers are inconsistent')) frappe.throw(_('The number of shares and the share numbers are inconsistent'))
if self.amount is None: if not self.amount:
self.amount = self.rate * self.no_of_shares self.amount = self.rate * self.no_of_shares
if self.amount != self.rate * self.no_of_shares: if self.amount != self.rate * self.no_of_shares:
frappe.throw(_('There are inconsistencies between the rate, no of shares and the amount calculated')) frappe.throw(_('There are inconsistencies between the rate, no of shares and the amount calculated'))
def share_exists(self, shareholder): def share_exists(self, shareholder):
# return True if exits, doc = self.get_shareholder_doc(shareholder)
# False if completely doesn't exist,
# 'partially exists' if partailly doesn't exist
ret_val = self.recursive_share_check(shareholder, self.share_type,
query = {
'from_no': self.from_no,
'to_no': self.to_no
}
)
if all(boolean == True for boolean in ret_val):
return True
elif True in ret_val:
return 'partially exists'
else:
return False
def recursive_share_check(self, shareholder, share_type, query):
# query = {'from_no': share_starting_no, 'to_no': share_ending_no}
# Recursive check if a given part of shares is held by the shareholder
# return a list containing True and False
# Eg. [True, False, True]
# All True implies its completely inside
# All False implies its completely outside
# A mix implies its partially inside/outside
does_share_exist = []
doc = frappe.get_doc('Shareholder', shareholder)
for entry in doc.share_balance: for entry in doc.share_balance:
if entry.share_type != share_type or \ if entry.share_type != self.share_type or \
entry.from_no > query['to_no'] or \ entry.from_no > self.to_no or \
entry.to_no < query['from_no']: entry.to_no < self.from_no:
continue # since query lies outside bounds continue # since query lies outside bounds
elif entry.from_no <= query['from_no'] and entry.to_no >= query['to_no']: elif entry.from_no <= self.from_no and entry.to_no >= self.to_no: #both inside
return [True] # absolute truth! return 'Complete' # absolute truth!
elif entry.from_no >= query['from_no'] and entry.to_no <= query['to_no']: elif entry.from_no <= self.from_no <= self.to_no:
# split and check return 'Partial'
does_share_exist.extend(self.recursive_share_check(shareholder, elif entry.from_no <= self.to_no <= entry.to_no:
share_type, return 'Partial'
{
'from_no': query['from_no'],
'to_no': entry.from_no - 1
}
))
does_share_exist.append(True)
does_share_exist.extend(self.recursive_share_check(shareholder,
share_type,
{
'from_no': entry.to_no + 1,
'to_no': query['to_no']
}
))
elif query['from_no'] <= entry.from_no <= query['to_no'] and entry.to_no >= query['to_no']:
does_share_exist.extend(self.recursive_share_check(shareholder,
share_type,
{
'from_no': query['from_no'],
'to_no': entry.from_no - 1
}
))
elif query['from_no'] <= entry.to_no <= query['to_no'] and entry.from_no <= query['from_no']:
does_share_exist.extend(self.recursive_share_check(shareholder,
share_type,
{
'from_no': entry.to_no + 1,
'to_no': query['to_no']
}
))
does_share_exist.append(False) return 'Outside'
return does_share_exist
def folio_no_validation(self): def folio_no_validation(self):
shareholders = ['from_shareholder', 'to_shareholder'] shareholders = ['from_shareholder', 'to_shareholder']
shareholders = [shareholder for shareholder in shareholders if self.get(shareholder) is not ''] shareholders = [shareholder for shareholder in shareholders if self.get(shareholder) is not '']
for shareholder in shareholders: for shareholder in shareholders:
doc = frappe.get_doc('Shareholder', self.get(shareholder)) doc = self.get_shareholder_doc(self.get(shareholder))
if doc.company != self.company: if doc.company != self.company:
frappe.throw(_('The shareholder does not belong to this company')) frappe.throw(_('The shareholder does not belong to this company'))
if doc.folio_no is '' or doc.folio_no is None: if not doc.folio_no:
doc.folio_no = self.from_folio_no \ doc.folio_no = self.from_folio_no \
if (shareholder == 'from_shareholder') else self.to_folio_no; if (shareholder == 'from_shareholder') else self.to_folio_no
doc.save() doc.save()
else: else:
if doc.folio_no and doc.folio_no != (self.from_folio_no if (shareholder == 'from_shareholder') else self.to_folio_no): if doc.folio_no and doc.folio_no != (self.from_folio_no if (shareholder == 'from_shareholder') else self.to_folio_no):
@@ -200,24 +184,14 @@ class ShareTransfer(Document):
def autoname_folio(self, shareholder, is_company=False): def autoname_folio(self, shareholder, is_company=False):
if is_company: if is_company:
doc = self.get_shareholder_doc(shareholder) doc = self.get_company_shareholder()
else: else:
doc = frappe.get_doc('Shareholder' , shareholder) doc = self.get_shareholder_doc(shareholder)
doc.folio_no = make_autoname('FN.#####') doc.folio_no = make_autoname('FN.#####')
doc.save() doc.save()
return doc.folio_no return doc.folio_no
def remove_shares(self, shareholder): def remove_shares(self, shareholder):
self.iterative_share_removal(shareholder, self.share_type,
{
'from_no': self.from_no,
'to_no' : self.to_no
},
rate = self.rate,
amount = self.amount
)
def iterative_share_removal(self, shareholder, share_type, query, rate, amount):
# query = {'from_no': share_starting_no, 'to_no': share_ending_no} # query = {'from_no': share_starting_no, 'to_no': share_ending_no}
# Shares exist for sure # Shares exist for sure
# Iterate over all entries and modify entry if in entry # Iterate over all entries and modify entry if in entry
@@ -227,31 +201,31 @@ class ShareTransfer(Document):
for entry in current_entries: for entry in current_entries:
# use spaceage logic here # use spaceage logic here
if entry.share_type != share_type or \ if entry.share_type != self.share_type or \
entry.from_no > query['to_no'] or \ entry.from_no > self.to_no or \
entry.to_no < query['from_no']: entry.to_no < self.from_no:
new_entries.append(entry) new_entries.append(entry)
continue # since query lies outside bounds continue # since query lies outside bounds
elif entry.from_no <= query['from_no'] and entry.to_no >= query['to_no']: elif entry.from_no <= self.from_no and entry.to_no >= self.to_no:
#split #split
if entry.from_no == query['from_no']: if entry.from_no == self.from_no:
if entry.to_no == query['to_no']: if entry.to_no == self.to_no:
pass #nothing to append pass #nothing to append
else: else:
new_entries.append(self.return_share_balance_entry(query['to_no']+1, entry.to_no, entry.rate)) new_entries.append(self.return_share_balance_entry(self.to_no+1, entry.to_no, entry.rate))
else: else:
if entry.to_no == query['to_no']: if entry.to_no == self.to_no:
new_entries.append(self.return_share_balance_entry(entry.from_no, query['from_no']-1, entry.rate)) new_entries.append(self.return_share_balance_entry(entry.from_no, self.from_no-1, entry.rate))
else: else:
new_entries.append(self.return_share_balance_entry(entry.from_no, query['from_no']-1, entry.rate)) new_entries.append(self.return_share_balance_entry(entry.from_no, self.from_no-1, entry.rate))
new_entries.append(self.return_share_balance_entry(query['to_no']+1, entry.to_no, entry.rate)) new_entries.append(self.return_share_balance_entry(self.to_no+1, entry.to_no, entry.rate))
elif entry.from_no >= query['from_no'] and entry.to_no <= query['to_no']: elif entry.from_no >= self.from_no and entry.to_no <= self.to_no:
# split and check # split and check
pass #nothing to append pass #nothing to append
elif query['from_no'] <= entry.from_no <= query['to_no'] and entry.to_no >= query['to_no']: elif self.from_no <= entry.from_no <= self.to_no and entry.to_no >= self.to_no:
new_entries.append(self.return_share_balance_entry(query['to_no']+1, entry.to_no, entry.rate)) new_entries.append(self.return_share_balance_entry(self.to_no+1, entry.to_no, entry.rate))
elif query['from_no'] <= entry.to_no <= query['to_no'] and entry.from_no <= query['from_no']: elif self.from_no <= entry.to_no <= self.to_no and entry.from_no <= self.from_no:
new_entries.append(self.return_share_balance_entry(entry.from_no, query['from_no']-1, entry.rate)) new_entries.append(self.return_share_balance_entry(entry.from_no, self.from_no-1, entry.rate))
else: else:
new_entries.append(entry) new_entries.append(entry)
@@ -272,16 +246,34 @@ class ShareTransfer(Document):
} }
def get_shareholder_doc(self, shareholder): def get_shareholder_doc(self, shareholder):
# Get Shareholder doc based on the Shareholder title # Get Shareholder doc based on the Shareholder name
doc = frappe.get_list('Shareholder', if shareholder:
filters = [ query_filters = {'name': shareholder}
('Shareholder', 'title', '=', shareholder)
] name = frappe.db.get_value('Shareholder', {'name': shareholder}, 'name')
)
if len(doc) == 1: return frappe.get_doc('Shareholder', name)
return frappe.get_doc('Shareholder', doc[0]['name'])
else: #It will necessarily by 0 indicating it doesn't exist def get_company_shareholder(self):
return False # Get company doc or create one if not present
company_shareholder = frappe.db.get_value('Shareholder',
{
'company': self.company,
'is_company': 1
}, 'name')
if company_shareholder:
return frappe.get_doc('Shareholder', company_shareholder)
else:
shareholder = frappe.get_doc({
'doctype': 'Shareholder',
'title': self.company,
'company': self.company,
'is_company': 1
})
shareholder.insert()
return shareholder
@frappe.whitelist() @frappe.whitelist()
def make_jv_entry( company, account, amount, payment_account,\ def make_jv_entry( company, account, amount, payment_account,\

View File

@@ -15,73 +15,73 @@ class TestShareTransfer(unittest.TestCase):
frappe.db.sql("delete from `tabShare Balance`") frappe.db.sql("delete from `tabShare Balance`")
share_transfers = [ share_transfers = [
{ {
"doctype" : "Share Transfer", "doctype": "Share Transfer",
"transfer_type" : "Issue", "transfer_type": "Issue",
"date" : "2018-01-01", "date": "2018-01-01",
"to_shareholder" : "SH-00001", "to_shareholder": "SH-00001",
"share_type" : "Equity", "share_type": "Equity",
"from_no" : 1, "from_no": 1,
"to_no" : 500, "to_no": 500,
"no_of_shares" : 500, "no_of_shares": 500,
"rate" : 10, "rate": 10,
"company" : "_Test Company", "company": "_Test Company",
"asset_account" : "Cash - _TC", "asset_account": "Cash - _TC",
"equity_or_liability_account": "Creditors - _TC" "equity_or_liability_account": "Creditors - _TC"
}, },
{ {
"doctype" : "Share Transfer", "doctype": "Share Transfer",
"transfer_type" : "Transfer", "transfer_type": "Transfer",
"date" : "2018-01-02", "date": "2018-01-02",
"from_shareholder" : "SH-00001", "from_shareholder": "SH-00001",
"to_shareholder" : "SH-00002", "to_shareholder": "SH-00002",
"share_type" : "Equity", "share_type": "Equity",
"from_no" : 101, "from_no": 101,
"to_no" : 200, "to_no": 200,
"no_of_shares" : 100, "no_of_shares": 100,
"rate" : 15, "rate": 15,
"company" : "_Test Company", "company": "_Test Company",
"equity_or_liability_account": "Creditors - _TC" "equity_or_liability_account": "Creditors - _TC"
}, },
{ {
"doctype" : "Share Transfer", "doctype": "Share Transfer",
"transfer_type" : "Transfer", "transfer_type": "Transfer",
"date" : "2018-01-03", "date": "2018-01-03",
"from_shareholder" : "SH-00001", "from_shareholder": "SH-00001",
"to_shareholder" : "SH-00003", "to_shareholder": "SH-00003",
"share_type" : "Equity", "share_type": "Equity",
"from_no" : 201, "from_no": 201,
"to_no" : 500, "to_no": 500,
"no_of_shares" : 300, "no_of_shares": 300,
"rate" : 20, "rate": 20,
"company" : "_Test Company", "company": "_Test Company",
"equity_or_liability_account": "Creditors - _TC" "equity_or_liability_account": "Creditors - _TC"
}, },
{ {
"doctype" : "Share Transfer", "doctype": "Share Transfer",
"transfer_type" : "Transfer", "transfer_type": "Transfer",
"date" : "2018-01-04", "date": "2018-01-04",
"from_shareholder" : "SH-00003", "from_shareholder": "SH-00003",
"to_shareholder" : "SH-00002", "to_shareholder": "SH-00002",
"share_type" : "Equity", "share_type": "Equity",
"from_no" : 201, "from_no": 201,
"to_no" : 400, "to_no": 400,
"no_of_shares" : 200, "no_of_shares": 200,
"rate" : 15, "rate": 15,
"company" : "_Test Company", "company": "_Test Company",
"equity_or_liability_account": "Creditors - _TC" "equity_or_liability_account": "Creditors - _TC"
}, },
{ {
"doctype" : "Share Transfer", "doctype": "Share Transfer",
"transfer_type" : "Purchase", "transfer_type": "Purchase",
"date" : "2018-01-05", "date": "2018-01-05",
"from_shareholder" : "SH-00003", "from_shareholder": "SH-00003",
"share_type" : "Equity", "share_type": "Equity",
"from_no" : 401, "from_no": 401,
"to_no" : 500, "to_no": 500,
"no_of_shares" : 100, "no_of_shares": 100,
"rate" : 25, "rate": 25,
"company" : "_Test Company", "company": "_Test Company",
"asset_account" : "Cash - _TC", "asset_account": "Cash - _TC",
"equity_or_liability_account": "Creditors - _TC" "equity_or_liability_account": "Creditors - _TC"
} }
] ]
@@ -91,33 +91,33 @@ class TestShareTransfer(unittest.TestCase):
def test_invalid_share_transfer(self): def test_invalid_share_transfer(self):
doc = frappe.get_doc({ doc = frappe.get_doc({
"doctype" : "Share Transfer", "doctype": "Share Transfer",
"transfer_type" : "Transfer", "transfer_type": "Transfer",
"date" : "2018-01-05", "date": "2018-01-05",
"from_shareholder" : "SH-00003", "from_shareholder": "SH-00003",
"to_shareholder" : "SH-00002", "to_shareholder": "SH-00002",
"share_type" : "Equity", "share_type": "Equity",
"from_no" : 1, "from_no": 1,
"to_no" : 100, "to_no": 100,
"no_of_shares" : 100, "no_of_shares": 100,
"rate" : 15, "rate": 15,
"company" : "_Test Company", "company": "_Test Company",
"equity_or_liability_account": "Creditors - _TC" "equity_or_liability_account": "Creditors - _TC"
}) })
self.assertRaises(ShareDontExists, doc.insert) self.assertRaises(ShareDontExists, doc.insert)
doc = frappe.get_doc({ doc = frappe.get_doc({
"doctype" : "Share Transfer", "doctype": "Share Transfer",
"transfer_type" : "Purchase", "transfer_type": "Purchase",
"date" : "2018-01-02", "date": "2018-01-02",
"from_shareholder" : "SH-00001", "from_shareholder": "SH-00001",
"share_type" : "Equity", "share_type": "Equity",
"from_no" : 1, "from_no": 1,
"to_no" : 200, "to_no": 200,
"no_of_shares" : 200, "no_of_shares": 200,
"rate" : 15, "rate": 15,
"company" : "_Test Company", "company": "_Test Company",
"asset_account" : "Cash - _TC", "asset_account": "Cash - _TC",
"equity_or_liability_account": "Creditors - _TC" "equity_or_liability_account": "Creditors - _TC"
}) })
self.assertRaises(ShareDontExists, doc.insert) self.assertRaises(ShareDontExists, doc.insert)

View File

@@ -1,587 +1,163 @@
{ {
"allow_copy": 0, "autoname": "naming_series:",
"allow_guest_to_view": 0, "creation": "2017-12-25 16:50:53.878430",
"allow_import": 0, "doctype": "DocType",
"allow_rename": 0, "editable_grid": 1,
"autoname": "naming_series:", "engine": "InnoDB",
"beta": 0, "field_order": [
"creation": "2017-12-25 16:50:53.878430", "title",
"custom": 0, "column_break_2",
"description": "", "naming_series",
"docstatus": 0, "section_break_2",
"doctype": "DocType", "folio_no",
"document_type": "", "column_break_4",
"editable_grid": 1, "company",
"engine": "InnoDB", "is_company",
"address_contacts",
"address_html",
"column_break_9",
"contact_html",
"section_break_3",
"share_balance",
"contact_list"
],
"fields": [ "fields": [
{ {
"allow_bulk_edit": 0, "fieldname": "title",
"allow_in_quick_entry": 0, "fieldtype": "Data",
"allow_on_submit": 0, "label": "Title",
"bold": 0, "reqd": 1
"collapsible": 0, },
"columns": 0,
"fieldname": "title",
"fieldtype": "Data",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Title",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{ {
"allow_bulk_edit": 0, "fieldname": "column_break_2",
"allow_in_quick_entry": 0, "fieldtype": "Column Break"
"allow_on_submit": 0, },
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "column_break_2",
"fieldtype": "Column Break",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{ {
"allow_bulk_edit": 0, "fieldname": "naming_series",
"allow_in_quick_entry": 0, "fieldtype": "Select",
"allow_on_submit": 0, "options": "ACC-SH-.YYYY.-"
"bold": 0, },
"collapsible": 0,
"columns": 0,
"default": "",
"fieldname": "naming_series",
"fieldtype": "Select",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "",
"length": 0,
"no_copy": 0,
"options": "ACC-SH-.YYYY.-",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{ {
"allow_bulk_edit": 0, "fieldname": "section_break_2",
"allow_in_quick_entry": 0, "fieldtype": "Section Break"
"allow_on_submit": 0, },
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "section_break_2",
"fieldtype": "Section Break",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{ {
"allow_bulk_edit": 0, "fieldname": "folio_no",
"allow_in_quick_entry": 0, "fieldtype": "Data",
"allow_on_submit": 0, "label": "Folio no.",
"bold": 0, "read_only": 1,
"collapsible": 0,
"columns": 0,
"fieldname": "folio_no",
"fieldtype": "Data",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Folio no.",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 1,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 1 "unique": 1
}, },
{ {
"allow_bulk_edit": 0, "fieldname": "column_break_4",
"allow_in_quick_entry": 0, "fieldtype": "Column Break"
"allow_on_submit": 0, },
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "column_break_4",
"fieldtype": "Column Break",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{ {
"allow_bulk_edit": 0, "fieldname": "company",
"allow_in_quick_entry": 0, "fieldtype": "Link",
"allow_on_submit": 0, "in_list_view": 1,
"bold": 0, "label": "Company",
"collapsible": 0, "options": "Company",
"columns": 0, "reqd": 1
"fieldname": "company", },
"fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Company",
"length": 0,
"no_copy": 0,
"options": "Company",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{ {
"allow_bulk_edit": 0, "default": "0",
"allow_in_quick_entry": 0, "fieldname": "is_company",
"allow_on_submit": 0, "fieldtype": "Check",
"bold": 0, "hidden": 1,
"collapsible": 0, "label": "Is Company",
"columns": 0, "read_only": 1
"fieldname": "is_company", },
"fieldtype": "Check",
"hidden": 1,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Is Company",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 1,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{ {
"allow_bulk_edit": 0, "fieldname": "address_contacts",
"allow_in_quick_entry": 0, "fieldtype": "Section Break",
"allow_on_submit": 0, "label": "Address and Contacts",
"bold": 0, "options": "fa fa-map-marker"
"collapsible": 0, },
"columns": 0,
"fieldname": "address_contacts",
"fieldtype": "Section Break",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Address and Contacts",
"length": 0,
"no_copy": 0,
"options": "fa fa-map-marker",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{ {
"allow_bulk_edit": 0, "fieldname": "address_html",
"allow_in_quick_entry": 0, "fieldtype": "HTML",
"allow_on_submit": 0, "label": "Address HTML",
"bold": 0, "read_only": 1
"collapsible": 0, },
"columns": 0,
"fieldname": "address_html",
"fieldtype": "HTML",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Address HTML",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 1,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{ {
"allow_bulk_edit": 0, "fieldname": "column_break_9",
"allow_in_quick_entry": 0, "fieldtype": "Column Break"
"allow_on_submit": 0, },
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "column_break_9",
"fieldtype": "Column Break",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{ {
"allow_bulk_edit": 0, "fieldname": "contact_html",
"allow_in_quick_entry": 0, "fieldtype": "HTML",
"allow_on_submit": 0, "label": "Contact HTML",
"bold": 0, "read_only": 1
"collapsible": 0, },
"columns": 0,
"fieldname": "contact_html",
"fieldtype": "HTML",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Contact HTML",
"length": 0,
"no_copy": 0,
"options": "",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 1,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{ {
"allow_bulk_edit": 0, "fieldname": "section_break_3",
"allow_in_quick_entry": 0, "fieldtype": "Section Break",
"allow_on_submit": 0, "label": "Share Balance"
"bold": 0, },
"collapsible": 0,
"columns": 0,
"fieldname": "section_break_3",
"fieldtype": "Section Break",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Share Balance",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{ {
"allow_bulk_edit": 0, "fieldname": "share_balance",
"allow_in_quick_entry": 0, "fieldtype": "Table",
"allow_on_submit": 0, "label": "Share Balance",
"bold": 0, "options": "Share Balance",
"collapsible": 0, "read_only": 1
"columns": 0, },
"fieldname": "share_balance",
"fieldtype": "Table",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Share Balance",
"length": 0,
"no_copy": 0,
"options": "Share Balance",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 1,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{ {
"allow_bulk_edit": 0, "description": "Hidden list maintaining the list of contacts linked to Shareholder",
"allow_in_quick_entry": 0, "fieldname": "contact_list",
"allow_on_submit": 0, "fieldtype": "Code",
"bold": 0, "hidden": 1,
"collapsible": 0, "label": "Contact List",
"columns": 0, "read_only": 1
"description": "Hidden list maintaining the list of contacts linked to Shareholder",
"fieldname": "contact_list",
"fieldtype": "Code",
"hidden": 1,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Contact List",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 1,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
} }
], ],
"has_web_view": 0, "modified": "2019-11-17 23:24:11.395882",
"hide_heading": 0, "modified_by": "Administrator",
"hide_toolbar": 0, "module": "Accounts",
"idx": 0, "name": "Shareholder",
"image_view": 0, "name_case": "Title Case",
"in_create": 0, "owner": "Administrator",
"is_submittable": 0,
"issingle": 0,
"istable": 0,
"max_attachments": 0,
"modified": "2018-09-18 14:14:24.953014",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Shareholder",
"name_case": "Title Case",
"owner": "Administrator",
"permissions": [ "permissions": [
{ {
"amend": 0, "create": 1,
"cancel": 0, "delete": 1,
"create": 1, "email": 1,
"delete": 1, "export": 1,
"email": 1, "print": 1,
"export": 1, "read": 1,
"if_owner": 0, "report": 1,
"import": 0, "role": "System Manager",
"permlevel": 0, "share": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"set_user_permissions": 0,
"share": 1,
"submit": 0,
"write": 1 "write": 1
}, },
{ {
"amend": 0, "create": 1,
"cancel": 0, "delete": 1,
"create": 1, "email": 1,
"delete": 1, "export": 1,
"email": 1, "print": 1,
"export": 1, "read": 1,
"if_owner": 0, "report": 1,
"import": 0, "role": "Accounts Manager",
"permlevel": 0, "share": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Accounts Manager",
"set_user_permissions": 0,
"share": 1,
"submit": 0,
"write": 1 "write": 1
}, },
{ {
"amend": 0, "create": 1,
"cancel": 0, "delete": 1,
"create": 1, "email": 1,
"delete": 1, "export": 1,
"email": 1, "print": 1,
"export": 1, "read": 1,
"if_owner": 0, "report": 1,
"import": 0, "role": "Accounts User",
"permlevel": 0, "share": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Accounts User",
"set_user_permissions": 0,
"share": 1,
"submit": 0,
"write": 1 "write": 1
} }
], ],
"quick_entry": 0, "search_fields": "folio_no",
"read_only": 0, "sort_field": "modified",
"read_only_onload": 0, "sort_order": "DESC",
"search_fields": "folio_no", "title_field": "title",
"show_name_in_global_search": 0, "track_changes": 1
"sort_field": "modified",
"sort_order": "DESC",
"title_field": "title",
"track_changes": 1,
"track_seen": 0,
"track_views": 0
} }

View File

@@ -70,7 +70,7 @@ class ShippingRule(Document):
def get_shipping_amount_from_rules(self, value): def get_shipping_amount_from_rules(self, value):
for condition in self.get("conditions"): for condition in self.get("conditions"):
if not condition.to_value or (flt(condition.from_value) <= value <= flt(condition.to_value)): if not condition.to_value or (flt(condition.from_value) <= flt(value) <= flt(condition.to_value)):
return condition.shipping_amount return condition.shipping_amount
return 0.0 return 0.0

View File

@@ -90,8 +90,12 @@ def merge_similar_entries(gl_map):
else: else:
merged_gl_map.append(entry) merged_gl_map.append(entry)
company = gl_map[0].company if gl_map else erpnext.get_default_company()
company_currency = erpnext.get_company_currency(company)
precision = get_field_precision(frappe.get_meta("GL Entry").get_field("debit"), company_currency)
# filter zero debit and credit entries # filter zero debit and credit entries
merged_gl_map = filter(lambda x: flt(x.debit, 9)!=0 or flt(x.credit, 9)!=0, merged_gl_map) merged_gl_map = filter(lambda x: flt(x.debit, precision)!=0 or flt(x.credit, precision)!=0, merged_gl_map)
merged_gl_map = list(merged_gl_map) merged_gl_map = list(merged_gl_map)
return merged_gl_map return merged_gl_map

View File

@@ -23,7 +23,7 @@ class DuplicatePartyAccountError(frappe.ValidationError): pass
@frappe.whitelist() @frappe.whitelist()
def get_party_details(party=None, account=None, party_type="Customer", company=None, posting_date=None, def get_party_details(party=None, account=None, party_type="Customer", company=None, posting_date=None,
bill_date=None, price_list=None, currency=None, doctype=None, ignore_permissions=False, fetch_payment_terms_template=True, bill_date=None, price_list=None, currency=None, doctype=None, ignore_permissions=False, fetch_payment_terms_template=True,
party_address=None, shipping_address=None, pos_profile=None): party_address=None, company_address=None, shipping_address=None, pos_profile=None):
if not party: if not party:
return {} return {}
@@ -31,14 +31,14 @@ def get_party_details(party=None, account=None, party_type="Customer", company=N
frappe.throw(_("{0}: {1} does not exists").format(party_type, party)) frappe.throw(_("{0}: {1} does not exists").format(party_type, party))
return _get_party_details(party, account, party_type, return _get_party_details(party, account, party_type,
company, posting_date, bill_date, price_list, currency, doctype, ignore_permissions, company, posting_date, bill_date, price_list, currency, doctype, ignore_permissions,
fetch_payment_terms_template, party_address, shipping_address, pos_profile) fetch_payment_terms_template, party_address, company_address, shipping_address, pos_profile)
def _get_party_details(party=None, account=None, party_type="Customer", company=None, posting_date=None, def _get_party_details(party=None, account=None, party_type="Customer", company=None, posting_date=None,
bill_date=None, price_list=None, currency=None, doctype=None, ignore_permissions=False, bill_date=None, price_list=None, currency=None, doctype=None, ignore_permissions=False,
fetch_payment_terms_template=True, party_address=None, shipping_address=None, pos_profile=None): fetch_payment_terms_template=True, party_address=None, company_address=None,shipping_address=None, pos_profile=None):
out = frappe._dict(set_account_and_due_date(party, account, party_type, company, posting_date, bill_date, doctype)) party_details = frappe._dict(set_account_and_due_date(party, account, party_type, company, posting_date, bill_date, doctype))
party = out[party_type.lower()] party = party_details[party_type.lower()]
if not ignore_permissions and not frappe.has_permission(party_type, "read", party): if not ignore_permissions and not frappe.has_permission(party_type, "read", party):
frappe.throw(_("Not permitted for {0}").format(party), frappe.PermissionError) frappe.throw(_("Not permitted for {0}").format(party), frappe.PermissionError)
@@ -46,76 +46,81 @@ def _get_party_details(party=None, account=None, party_type="Customer", company=
party = frappe.get_doc(party_type, party) party = frappe.get_doc(party_type, party)
currency = party.default_currency if party.get("default_currency") else get_company_currency(company) currency = party.default_currency if party.get("default_currency") else get_company_currency(company)
party_address, shipping_address = set_address_details(out, party, party_type, doctype, company, party_address, shipping_address) party_address, shipping_address = set_address_details(party_details, party, party_type, doctype, company, party_address, company_address, shipping_address)
set_contact_details(out, party, party_type) set_contact_details(party_details, party, party_type)
set_other_values(out, party, party_type) set_other_values(party_details, party, party_type)
set_price_list(out, party, party_type, price_list, pos_profile) set_price_list(party_details, party, party_type, price_list, pos_profile)
out["tax_category"] = get_address_tax_category(party.get("tax_category"), party_details["tax_category"] = get_address_tax_category(party.get("tax_category"),
party_address, shipping_address if party_type != "Supplier" else party_address) party_address, shipping_address if party_type != "Supplier" else party_address)
out["taxes_and_charges"] = set_taxes(party.name, party_type, posting_date, company,
customer_group=out.customer_group, supplier_group=out.supplier_group, tax_category=out.tax_category, if not party_details.get("taxes_and_charges"):
billing_address=party_address, shipping_address=shipping_address) party_details["taxes_and_charges"] = set_taxes(party.name, party_type, posting_date, company,
customer_group=party_details.customer_group, supplier_group=party_details.supplier_group, tax_category=party_details.tax_category,
billing_address=party_address, shipping_address=shipping_address)
if fetch_payment_terms_template: if fetch_payment_terms_template:
out["payment_terms_template"] = get_pyt_term_template(party.name, party_type, company) party_details["payment_terms_template"] = get_pyt_term_template(party.name, party_type, company)
if not out.get("currency"): if not party_details.get("currency"):
out["currency"] = currency party_details["currency"] = currency
# sales team # sales team
if party_type=="Customer": if party_type=="Customer":
out["sales_team"] = [{ party_details["sales_team"] = [{
"sales_person": d.sales_person, "sales_person": d.sales_person,
"allocated_percentage": d.allocated_percentage or None "allocated_percentage": d.allocated_percentage or None
} for d in party.get("sales_team")] } for d in party.get("sales_team")]
# supplier tax withholding category # supplier tax withholding category
if party_type == "Supplier" and party: if party_type == "Supplier" and party:
out["supplier_tds"] = frappe.get_value(party_type, party.name, "tax_withholding_category") party_details["supplier_tds"] = frappe.get_value(party_type, party.name, "tax_withholding_category")
return out return party_details
def set_address_details(out, party, party_type, doctype=None, company=None, party_address=None, shipping_address=None): def set_address_details(party_details, party, party_type, doctype=None, company=None, party_address=None, company_address=None, shipping_address=None):
billing_address_field = "customer_address" if party_type == "Lead" \ billing_address_field = "customer_address" if party_type == "Lead" \
else party_type.lower() + "_address" else party_type.lower() + "_address"
out[billing_address_field] = party_address or get_default_address(party_type, party.name) party_details[billing_address_field] = party_address or get_default_address(party_type, party.name)
if doctype: if doctype:
out.update(get_fetch_values(doctype, billing_address_field, out[billing_address_field])) party_details.update(get_fetch_values(doctype, billing_address_field, party_details[billing_address_field]))
# address display # address display
out.address_display = get_address_display(out[billing_address_field]) party_details.address_display = get_address_display(party_details[billing_address_field])
# shipping address # shipping address
if party_type in ["Customer", "Lead"]: if party_type in ["Customer", "Lead"]:
out.shipping_address_name = shipping_address or get_party_shipping_address(party_type, party.name) party_details.shipping_address_name = shipping_address or get_party_shipping_address(party_type, party.name)
out.shipping_address = get_address_display(out["shipping_address_name"]) party_details.shipping_address = get_address_display(party_details["shipping_address_name"])
if doctype: if doctype:
out.update(get_fetch_values(doctype, 'shipping_address_name', out.shipping_address_name)) party_details.update(get_fetch_values(doctype, 'shipping_address_name', party_details.shipping_address_name))
if doctype and doctype in ['Delivery Note', 'Sales Invoice']: if company_address:
out.update(get_company_address(company)) party_details.update({'company_address': company_address})
if out.company_address: else:
out.update(get_fetch_values(doctype, 'company_address', out.company_address)) party_details.update(get_company_address(company))
get_regional_address_details(out, doctype, company)
elif doctype and doctype == "Purchase Invoice": if doctype and doctype in ['Delivery Note', 'Sales Invoice', 'Sales Order']:
out.update(get_company_address(company)) if party_details.company_address:
if out.company_address: party_details.update(get_fetch_values(doctype, 'company_address', party_details.company_address))
out["shipping_address"] = shipping_address or out["company_address"] get_regional_address_details(party_details, doctype, company)
out.shipping_address_display = get_address_display(out["shipping_address"])
out.update(get_fetch_values(doctype, 'shipping_address', out.shipping_address))
get_regional_address_details(out, doctype, company)
return out.get(billing_address_field), out.shipping_address_name elif doctype and doctype in ["Purchase Invoice", "Purchase Order", "Purchase Receipt"]:
if party_details.company_address:
party_details["shipping_address"] = shipping_address or party_details["company_address"]
party_details.shipping_address_display = get_address_display(party_details["shipping_address"])
party_details.update(get_fetch_values(doctype, 'shipping_address', party_details.shipping_address))
get_regional_address_details(party_details, doctype, company)
return party_details.get(billing_address_field), party_details.shipping_address_name
@erpnext.allow_regional @erpnext.allow_regional
def get_regional_address_details(out, doctype, company): def get_regional_address_details(party_details, doctype, company):
pass pass
def set_contact_details(out, party, party_type): def set_contact_details(party_details, party, party_type):
out.contact_person = get_default_contact(party_type, party.name) party_details.contact_person = get_default_contact(party_type, party.name)
if not out.contact_person: if not party_details.contact_person:
out.update({ party_details.update({
"contact_person": None, "contact_person": None,
"contact_display": None, "contact_display": None,
"contact_email": None, "contact_email": None,
@@ -125,22 +130,22 @@ def set_contact_details(out, party, party_type):
"contact_department": None "contact_department": None
}) })
else: else:
out.update(get_contact_details(out.contact_person)) party_details.update(get_contact_details(party_details.contact_person))
def set_other_values(out, party, party_type): def set_other_values(party_details, party, party_type):
# copy # copy
if party_type=="Customer": if party_type=="Customer":
to_copy = ["customer_name", "customer_group", "territory", "language"] to_copy = ["customer_name", "customer_group", "territory", "language"]
else: else:
to_copy = ["supplier_name", "supplier_group", "language"] to_copy = ["supplier_name", "supplier_group", "language"]
for f in to_copy: for f in to_copy:
out[f] = party.get(f) party_details[f] = party.get(f)
# fields prepended with default in Customer doctype # fields prepended with default in Customer doctype
for f in ['currency'] \ for f in ['currency'] \
+ (['sales_partner', 'commission_rate'] if party_type=="Customer" else []): + (['sales_partner', 'commission_rate'] if party_type=="Customer" else []):
if party.get("default_" + f): if party.get("default_" + f):
out[f] = party.get("default_" + f) party_details[f] = party.get("default_" + f)
def get_default_price_list(party): def get_default_price_list(party):
"""Return default price list for party (Document object)""" """Return default price list for party (Document object)"""
@@ -155,7 +160,7 @@ def get_default_price_list(party):
return None return None
def set_price_list(out, party, party_type, given_price_list, pos=None): def set_price_list(party_details, party, party_type, given_price_list, pos=None):
# price list # price list
price_list = get_permitted_documents('Price List') price_list = get_permitted_documents('Price List')
@@ -173,9 +178,9 @@ def set_price_list(out, party, party_type, given_price_list, pos=None):
price_list = get_default_price_list(party) or given_price_list price_list = get_default_price_list(party) or given_price_list
if price_list: if price_list:
out.price_list_currency = frappe.db.get_value("Price List", price_list, "currency", cache=True) party_details.price_list_currency = frappe.db.get_value("Price List", price_list, "currency", cache=True)
out["selling_price_list" if party.doctype=="Customer" else "buying_price_list"] = price_list party_details["selling_price_list" if party.doctype=="Customer" else "buying_price_list"] = price_list
def set_account_and_due_date(party, account, party_type, company, posting_date, bill_date, doctype): def set_account_and_due_date(party, account, party_type, company, posting_date, bill_date, doctype):

View File

@@ -100,6 +100,11 @@ frappe.query_reports["Accounts Payable"] = {
"fieldtype": "Link", "fieldtype": "Link",
"options": "Supplier Group" "options": "Supplier Group"
}, },
{
"fieldname":"based_on_payment_terms",
"label": __("Based On Payment Terms"),
"fieldtype": "Check",
},
{ {
"fieldname":"tax_id", "fieldname":"tax_id",
"label": __("Tax Id"), "label": __("Tax Id"),

View File

@@ -88,6 +88,11 @@ frappe.query_reports["Accounts Payable Summary"] = {
"label": __("Supplier Group"), "label": __("Supplier Group"),
"fieldtype": "Link", "fieldtype": "Link",
"options": "Supplier Group" "options": "Supplier Group"
},
{
"fieldname":"based_on_payment_terms",
"label": __("Based On Payment Terms"),
"fieldtype": "Check",
} }
], ],

View File

@@ -60,6 +60,7 @@ class ReceivablePayableReport(object):
def get_data(self): def get_data(self):
self.get_gl_entries() self.get_gl_entries()
self.get_sales_invoices_or_customers_based_on_sales_person()
self.voucher_balance = OrderedDict() self.voucher_balance = OrderedDict()
self.init_voucher_balance() # invoiced, paid, credit_note, outstanding self.init_voucher_balance() # invoiced, paid, credit_note, outstanding
@@ -103,12 +104,18 @@ class ReceivablePayableReport(object):
def get_invoices(self, gle): def get_invoices(self, gle):
if gle.voucher_type in ('Sales Invoice', 'Purchase Invoice'): if gle.voucher_type in ('Sales Invoice', 'Purchase Invoice'):
self.invoices.add(gle.voucher_no) if self.filters.get("sales_person"):
if gle.voucher_no in self.sales_person_records.get("Sales Invoice", []) \
or gle.party in self.sales_person_records.get("Customer", []):
self.invoices.add(gle.voucher_no)
else:
self.invoices.add(gle.voucher_no)
def update_voucher_balance(self, gle): def update_voucher_balance(self, gle):
# get the row where this balance needs to be updated # get the row where this balance needs to be updated
# if its a payment, it will return the linked invoice or will be considered as advance # if its a payment, it will return the linked invoice or will be considered as advance
row = self.get_voucher_balance(gle) row = self.get_voucher_balance(gle)
if not row: return
# gle_balance will be the total "debit - credit" for receivable type reports and # gle_balance will be the total "debit - credit" for receivable type reports and
# and vice-versa for payable type reports # and vice-versa for payable type reports
gle_balance = self.get_gle_balance(gle) gle_balance = self.get_gle_balance(gle)
@@ -129,8 +136,13 @@ class ReceivablePayableReport(object):
row.paid -= gle_balance row.paid -= gle_balance
def get_voucher_balance(self, gle): def get_voucher_balance(self, gle):
voucher_balance = None if self.filters.get("sales_person"):
against_voucher = gle.against_voucher or gle.voucher_no
if not (gle.party in self.sales_person_records.get("Customer", []) or \
against_voucher in self.sales_person_records.get("Sales Invoice", [])):
return
voucher_balance = None
if gle.against_voucher: if gle.against_voucher:
# find invoice # find invoice
against_voucher = gle.against_voucher against_voucher = gle.against_voucher
@@ -318,7 +330,7 @@ class ReceivablePayableReport(object):
self.append_payment_term(row, d, term) self.append_payment_term(row, d, term)
def append_payment_term(self, row, d, term): def append_payment_term(self, row, d, term):
if self.filters.get("customer") and d.currency == d.party_account_currency: if (self.filters.get("customer") or self.filters.get("supplier")) and d.currency == d.party_account_currency:
invoiced = d.payment_amount invoiced = d.payment_amount
else: else:
invoiced = flt(flt(d.payment_amount) * flt(d.conversion_rate), self.currency_precision) invoiced = flt(flt(d.payment_amount) * flt(d.conversion_rate), self.currency_precision)
@@ -512,6 +524,22 @@ class ReceivablePayableReport(object):
order by posting_date, party""" order by posting_date, party"""
.format(select_fields, conditions), values, as_dict=True) .format(select_fields, conditions), values, as_dict=True)
def get_sales_invoices_or_customers_based_on_sales_person(self):
if self.filters.get("sales_person"):
lft, rgt = frappe.db.get_value("Sales Person",
self.filters.get("sales_person"), ["lft", "rgt"])
records = frappe.db.sql("""
select distinct parent, parenttype
from `tabSales Team` steam
where parenttype in ('Customer', 'Sales Invoice')
and exists(select name from `tabSales Person` where lft >= %s and rgt <= %s and name = steam.sales_person)
""", (lft, rgt), as_dict=1)
self.sales_person_records = frappe._dict()
for d in records:
self.sales_person_records.setdefault(d.parenttype, set()).add(d.parent)
def prepare_conditions(self): def prepare_conditions(self):
conditions = [""] conditions = [""]
values = [self.party_type, self.filters.report_date] values = [self.party_type, self.filters.report_date]
@@ -564,16 +592,6 @@ class ReceivablePayableReport(object):
conditions.append("party in (select name from tabCustomer where default_sales_partner=%s)") conditions.append("party in (select name from tabCustomer where default_sales_partner=%s)")
values.append(self.filters.get("sales_partner")) values.append(self.filters.get("sales_partner"))
if self.filters.get("sales_person"):
lft, rgt = frappe.db.get_value("Sales Person",
self.filters.get("sales_person"), ["lft", "rgt"])
conditions.append("""exists(select name from `tabSales Team` steam where
steam.sales_person in (select name from `tabSales Person` where lft >= {0} and rgt <= {1})
and ((steam.parent = voucher_no and steam.parenttype = voucher_type)
or (steam.parent = against_voucher and steam.parenttype = against_voucher_type)
or (steam.parent = party and steam.parenttype = 'Customer')))""".format(lft, rgt))
def add_supplier_filters(self, conditions, values): def add_supplier_filters(self, conditions, values):
if self.filters.get("supplier_group"): if self.filters.get("supplier_group"):
conditions.append("""party in (select name from tabSupplier conditions.append("""party in (select name from tabSupplier

View File

@@ -106,6 +106,11 @@ frappe.query_reports["Accounts Receivable Summary"] = {
"label": __("Sales Person"), "label": __("Sales Person"),
"fieldtype": "Link", "fieldtype": "Link",
"options": "Sales Person" "options": "Sales Person"
},
{
"fieldname":"based_on_payment_terms",
"label": __("Based On Payment Terms"),
"fieldtype": "Check",
} }
], ],

View File

@@ -36,7 +36,7 @@ class AccountsReceivableSummary(ReceivablePayableReport):
self.filters.report_date) or {} self.filters.report_date) or {}
for party, party_dict in iteritems(self.party_total): for party, party_dict in iteritems(self.party_total):
if party_dict.outstanding <= 0: if party_dict.outstanding == 0:
continue continue
row = frappe._dict() row = frappe._dict()

View File

@@ -1,5 +1,6 @@
{% {%
var report_columns = report.get_columns_for_print(); var report_columns = report.get_columns_for_print();
report_columns = report_columns.filter(col => !col.hidden);
if (report_columns.length > 8) { if (report_columns.length > 8) {
frappe.throw(__("Too many columns. Export the report and print it using a spreadsheet application.")); frappe.throw(__("Too many columns. Export the report and print it using a spreadsheet application."));
@@ -15,34 +16,35 @@
height: 37px; height: 37px;
} }
</style> </style>
{% var letterhead= filters.letter_head || (frappe.get_doc(":Company", filters.company) && frappe.get_doc(":Company", filters.company).default_letter_head) %}
{% if(letterhead) { %}
<div style="margin-bottom: 7px;" class="text-center">
{%= frappe.boot.letter_heads[letterhead].header %}
</div>
{% } %}
<h2 class="text-center">{%= __(report.report_name) %}</h2> <h2 class="text-center">{%= __(report.report_name) %}</h2>
<h3 class="text-center">{%= filters.company %}</h3> <h3 class="text-center">{%= filters.company %}</h3>
{% if 'cost_center' in filters %} {% if 'cost_center' in filters %}
<h3 class="text-center">{%= filters.cost_center %}</h3> <h3 class="text-center">{%= filters.cost_center %}</h3>
{% endif %} {% endif %}
<h3 class="text-center">{%= filters.fiscal_year %}</h3> <h3 class="text-center">{%= filters.fiscal_year %}</h3>
<h5 class="text-center">{%= __("Currency") %} : {%= filters.presentation_currency || erpnext.get_currency(filters.company) %} </h4> <h5 class="text-center">
{%= __("Currency") %} : {%= filters.presentation_currency || erpnext.get_currency(filters.company) %}
</h5>
{% if (filters.from_date) { %} {% if (filters.from_date) { %}
<h4 class="text-center">{%= frappe.datetime.str_to_user(filters.from_date) %} - {%= frappe.datetime.str_to_user(filters.to_date) %}</h3> <h5 class="text-center">
{%= frappe.datetime.str_to_user(filters.from_date) %} - {%= frappe.datetime.str_to_user(filters.to_date) %}
</h5>
{% } %} {% } %}
<hr> <hr>
<table class="table table-bordered"> <table class="table table-bordered">
<thead> <thead>
<tr> <tr>
<th style="width: {%= 100 - (report_columns.length - 2) * 13 %}%"></th> <th style="width: {%= 100 - (report_columns.length - 1) * 13 %}%"></th>
{% for(var i=2, l=report_columns.length; i<l; i++) { %} {% for (let i=1, l=report_columns.length; i<l; i++) { %}
<th class="text-right">{%= report_columns[i].label %}</th> <th class="text-right">{%= report_columns[i].label %}</th>
{% } %} {% } %}
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{% for(var j=0, k=data.length-1; j<k; j++) { %} {% for(let j=0, k=data.length-1; j<k; j++) { %}
{% {%
var row = data[j]; var row = data[j];
var row_class = data[j].parent_account ? "" : "financial-statements-important"; var row_class = data[j].parent_account ? "" : "financial-statements-important";
@@ -52,11 +54,11 @@
<td> <td>
<span style="padding-left: {%= cint(data[j].indent) * 2 %}em">{%= row.account_name %}</span> <span style="padding-left: {%= cint(data[j].indent) * 2 %}em">{%= row.account_name %}</span>
</td> </td>
{% for(var i=2, l=report_columns.length; i<l; i++) { %} {% for(let i=1, l=report_columns.length; i<l; i++) { %}
<td class="text-right"> <td class="text-right">
{% var fieldname = report_columns[i].fieldname; %} {% const fieldname = report_columns[i].fieldname; %}
{% if (!is_null(row[fieldname])) { %} {% if (!is_null(row[fieldname])) { %}
{%= format_currency(row[fieldname], filters.presentation_currency) %} {%= frappe.format(row[fieldname], report_columns[i], {}, row) %}
{% } %} {% } %}
</td> </td>
{% } %} {% } %}
@@ -64,4 +66,6 @@
{% } %} {% } %}
</tbody> </tbody>
</table> </table>
<p class="text-right text-muted">Printed On {%= frappe.datetime.str_to_user(frappe.datetime.get_datetime_as_string()) %}</p> <p class="text-right text-muted">
Printed On {%= frappe.datetime.str_to_user(frappe.datetime.get_datetime_as_string()) %}
</p>

View File

@@ -264,8 +264,8 @@ def filter_out_zero_value_rows(data, parent_children_map, show_zero_values=False
def add_total_row(out, root_type, balance_must_be, period_list, company_currency): def add_total_row(out, root_type, balance_must_be, period_list, company_currency):
total_row = { total_row = {
"account_name": "'" + _("Total {0} ({1})").format(_(root_type), _(balance_must_be)) + "'", "account_name": _("Total {0} ({1})").format(_(root_type), _(balance_must_be)),
"account": "'" + _("Total {0} ({1})").format(_(root_type), _(balance_must_be)) + "'", "account": _("Total {0} ({1})").format(_(root_type), _(balance_must_be)),
"currency": company_currency "currency": company_currency
} }

View File

@@ -18,14 +18,17 @@ def execute(filters=None):
return columns, data return columns, data
def get_data(filters, show_party_name): def get_data(filters, show_party_name):
party_name_field = "{0}_name".format(frappe.scrub(filters.get('party_type'))) if filters.get('party_type') in ('Customer', 'Supplier', 'Employee', 'Member'):
party_name_field = "{0}_name".format(frappe.scrub(filters.get('party_type')))
if filters.get('party_type') == 'Student': if filters.get('party_type') == 'Student':
party_name_field = 'first_name' party_name_field = 'first_name'
elif filters.get('party_type') == 'Shareholder': elif filters.get('party_type') == 'Shareholder':
party_name_field = 'title' party_name_field = 'title'
else:
party_name_field = 'name'
party_filters = {"name": filters.get("party")} if filters.get("party") else {} party_filters = {"name": filters.get("party")} if filters.get("party") else {}
parties = frappe.get_all(filters.get("party_type"), fields = ["name", party_name_field], parties = frappe.get_all(filters.get("party_type"), fields = ["name", party_name_field],
filters = party_filters, order_by="name") filters = party_filters, order_by="name")
company_currency = frappe.get_cached_value('Company', filters.company, "default_currency") company_currency = frappe.get_cached_value('Company', filters.company, "default_currency")
opening_balances = get_opening_balances(filters) opening_balances = get_opening_balances(filters)
@@ -70,7 +73,7 @@ def get_data(filters, show_party_name):
# totals # totals
for col in total_row: for col in total_row:
total_row[col] += row.get(col) total_row[col] += row.get(col)
row.update({ row.update({
"currency": company_currency "currency": company_currency
}) })
@@ -78,7 +81,7 @@ def get_data(filters, show_party_name):
has_value = False has_value = False
if (opening_debit or opening_credit or debit or credit or closing_debit or closing_credit): if (opening_debit or opening_credit or debit or credit or closing_debit or closing_credit):
has_value =True has_value =True
if cint(filters.show_zero_values) or has_value: if cint(filters.show_zero_values) or has_value:
data.append(row) data.append(row)
@@ -94,9 +97,9 @@ def get_data(filters, show_party_name):
def get_opening_balances(filters): def get_opening_balances(filters):
gle = frappe.db.sql(""" gle = frappe.db.sql("""
select party, sum(debit) as opening_debit, sum(credit) as opening_credit select party, sum(debit) as opening_debit, sum(credit) as opening_credit
from `tabGL Entry` from `tabGL Entry`
where company=%(company)s where company=%(company)s
and ifnull(party_type, '') = %(party_type)s and ifnull(party, '') != '' and ifnull(party_type, '') = %(party_type)s and ifnull(party, '') != ''
and (posting_date < %(from_date)s or ifnull(is_opening, 'No') = 'Yes') and (posting_date < %(from_date)s or ifnull(is_opening, 'No') = 'Yes')
group by party""", { group by party""", {
@@ -114,11 +117,11 @@ def get_opening_balances(filters):
def get_balances_within_period(filters): def get_balances_within_period(filters):
gle = frappe.db.sql(""" gle = frappe.db.sql("""
select party, sum(debit) as debit, sum(credit) as credit select party, sum(debit) as debit, sum(credit) as credit
from `tabGL Entry` from `tabGL Entry`
where company=%(company)s where company=%(company)s
and ifnull(party_type, '') = %(party_type)s and ifnull(party, '') != '' and ifnull(party_type, '') = %(party_type)s and ifnull(party, '') != ''
and posting_date >= %(from_date)s and posting_date <= %(to_date)s and posting_date >= %(from_date)s and posting_date <= %(to_date)s
and ifnull(is_opening, 'No') = 'No' and ifnull(is_opening, 'No') = 'No'
group by party""", { group by party""", {
"company": filters.company, "company": filters.company,

View File

@@ -569,7 +569,7 @@ def get_stock_and_account_balance(account=None, posting_date=None, company=None)
warehouse_account = get_warehouse_account_map(company) warehouse_account = get_warehouse_account_map(company)
account_balance = get_balance_on(account, posting_date, in_account_currency=False) account_balance = get_balance_on(account, posting_date, in_account_currency=False, ignore_account_permission=True)
related_warehouses = [wh for wh, wh_details in warehouse_account.items() related_warehouses = [wh for wh, wh_details in warehouse_account.items()
if wh_details.account == account and not wh_details.is_group] if wh_details.account == account and not wh_details.is_group]

View File

@@ -33,7 +33,7 @@ class Asset(AccountsController):
self.make_asset_movement() self.make_asset_movement()
if not self.booked_fixed_asset and is_cwip_accounting_enabled(self.asset_category): if not self.booked_fixed_asset and is_cwip_accounting_enabled(self.asset_category):
self.make_gl_entries() self.make_gl_entries()
def before_cancel(self): def before_cancel(self):
self.cancel_auto_gen_movement() self.cancel_auto_gen_movement()
@@ -43,7 +43,7 @@ class Asset(AccountsController):
self.set_status() self.set_status()
delete_gl_entries(voucher_type='Asset', voucher_no=self.name) delete_gl_entries(voucher_type='Asset', voucher_no=self.name)
self.db_set('booked_fixed_asset', 0) self.db_set('booked_fixed_asset', 0)
def validate_asset_and_reference(self): def validate_asset_and_reference(self):
if self.purchase_invoice or self.purchase_receipt: if self.purchase_invoice or self.purchase_receipt:
reference_doc = 'Purchase Invoice' if self.purchase_invoice else 'Purchase Receipt' reference_doc = 'Purchase Invoice' if self.purchase_invoice else 'Purchase Receipt'
@@ -51,8 +51,8 @@ class Asset(AccountsController):
reference_doc = frappe.get_doc(reference_doc, reference_name) reference_doc = frappe.get_doc(reference_doc, reference_name)
if reference_doc.get('company') != self.company: if reference_doc.get('company') != self.company:
frappe.throw(_("Company of asset {0} and purchase document {1} doesn't matches.").format(self.name, reference_doc.get('name'))) frappe.throw(_("Company of asset {0} and purchase document {1} doesn't matches.").format(self.name, reference_doc.get('name')))
if self.is_existing_asset and self.purchase_invoice: if self.is_existing_asset and self.purchase_invoice:
frappe.throw(_("Purchase Invoice cannot be made against an existing asset {0}").format(self.name)) frappe.throw(_("Purchase Invoice cannot be made against an existing asset {0}").format(self.name))
@@ -135,7 +135,7 @@ class Asset(AccountsController):
movement = frappe.get_doc('Asset Movement', movements[0].get('name')) movement = frappe.get_doc('Asset Movement', movements[0].get('name'))
movement.flags.ignore_validate = True movement.flags.ignore_validate = True
movement.cancel() movement.cancel()
def make_asset_movement(self): def make_asset_movement(self):
reference_doctype = 'Purchase Receipt' if self.purchase_receipt else 'Purchase Invoice' reference_doctype = 'Purchase Receipt' if self.purchase_receipt else 'Purchase Invoice'
reference_docname = self.purchase_receipt or self.purchase_invoice reference_docname = self.purchase_receipt or self.purchase_invoice
@@ -204,7 +204,7 @@ class Asset(AccountsController):
if has_pro_rata and n==0: if has_pro_rata and n==0:
depreciation_amount, days, months = get_pro_rata_amt(d, depreciation_amount, depreciation_amount, days, months = get_pro_rata_amt(d, depreciation_amount,
self.available_for_use_date, d.depreciation_start_date) self.available_for_use_date, d.depreciation_start_date)
# For first depr schedule date will be the start date # For first depr schedule date will be the start date
# so monthly schedule date is calculated by removing month difference between use date and start date # so monthly schedule date is calculated by removing month difference between use date and start date
monthly_schedule_date = add_months(d.depreciation_start_date, - months + 1) monthly_schedule_date = add_months(d.depreciation_start_date, - months + 1)
@@ -262,7 +262,7 @@ class Asset(AccountsController):
else: else:
date = add_months(monthly_schedule_date, r) date = add_months(monthly_schedule_date, r)
amount = depreciation_amount / month_range amount = depreciation_amount / month_range
self.append("schedules", { self.append("schedules", {
"schedule_date": date, "schedule_date": date,
"depreciation_amount": amount, "depreciation_amount": amount,
@@ -517,15 +517,18 @@ def update_maintenance_status():
asset.set_status('Out of Order') asset.set_status('Out of Order')
def make_post_gl_entry(): def make_post_gl_entry():
if not is_cwip_accounting_enabled(self.asset_category):
return
assets = frappe.db.sql_list(""" select name from `tabAsset` asset_categories = frappe.db.get_all('Asset Category', fields = ['name', 'enable_cwip_accounting'])
where ifnull(booked_fixed_asset, 0) = 0 and available_for_use_date = %s""", nowdate())
for asset in assets: for asset_category in asset_categories:
doc = frappe.get_doc('Asset', asset) if cint(asset_category.enable_cwip_accounting):
doc.make_gl_entries() assets = frappe.db.sql_list(""" select name from `tabAsset`
where asset_category = %s and ifnull(booked_fixed_asset, 0) = 0
and available_for_use_date = %s""", (asset_category.name, nowdate()))
for asset in assets:
doc = frappe.get_doc('Asset', asset)
doc.make_gl_entries()
def get_asset_naming_series(): def get_asset_naming_series():
meta = frappe.get_meta('Asset') meta = frappe.get_meta('Asset')
@@ -607,13 +610,19 @@ def get_asset_account(account_name, asset=None, asset_category=None, company=Non
if asset: if asset:
account = get_asset_category_account(account_name, asset=asset, account = get_asset_category_account(account_name, asset=asset,
asset_category = asset_category, company = company) asset_category = asset_category, company = company)
if not asset and not account:
account = get_asset_category_account(account_name, asset_category = asset_category, company = company)
if not account: if not account:
account = frappe.get_cached_value('Company', company, account_name) account = frappe.get_cached_value('Company', company, account_name)
if not account: if not account:
frappe.throw(_("Set {0} in asset category {1} or company {2}") if not asset_category:
.format(account_name.replace('_', ' ').title(), asset_category, company)) frappe.throw(_("Set {0} in company {2}").format(account_name.replace('_', ' ').title(), company))
else:
frappe.throw(_("Set {0} in asset category {1} or company {2}")
.format(account_name.replace('_', ' ').title(), asset_category, company))
return account return account
@@ -652,10 +661,10 @@ def make_journal_entry(asset_name):
def make_asset_movement(assets, purpose=None): def make_asset_movement(assets, purpose=None):
import json import json
from six import string_types from six import string_types
if isinstance(assets, string_types): if isinstance(assets, string_types):
assets = json.loads(assets) assets = json.loads(assets)
if len(assets) == 0: if len(assets) == 0:
frappe.throw(_('Atleast one asset has to be selected.')) frappe.throw(_('Atleast one asset has to be selected.'))

View File

@@ -26,5 +26,11 @@ frappe.query_reports["Fixed Asset Register"] = {
fieldtype: "Link", fieldtype: "Link",
options: "Finance Book" options: "Finance Book"
}, },
{
fieldname:"date",
label: __("Date"),
fieldtype: "Date",
default: frappe.datetime.get_today()
},
] ]
}; };

View File

@@ -4,7 +4,7 @@
from __future__ import unicode_literals from __future__ import unicode_literals
import frappe import frappe
from frappe import _ from frappe import _
from frappe.utils import cstr from frappe.utils import cstr, today, flt
def execute(filters=None): def execute(filters=None):
filters = frappe._dict(filters or {}) filters = frappe._dict(filters or {})
@@ -86,8 +86,8 @@ def get_columns(filters):
"width": 90 "width": 90
}, },
{ {
"label": _("Current Value"), "label": _("Asset Value"),
"fieldname": "current_value", "fieldname": "asset_value",
"options": "Currency", "options": "Currency",
"width": 90 "width": 90
}, },
@@ -114,7 +114,7 @@ def get_data(filters):
data = [] data = []
conditions = get_conditions(filters) conditions = get_conditions(filters)
current_value_map = get_finance_book_value_map(filters.finance_book) depreciation_amount_map = get_finance_book_value_map(filters.date, filters.finance_book)
pr_supplier_map = get_purchase_receipt_supplier_map() pr_supplier_map = get_purchase_receipt_supplier_map()
pi_supplier_map = get_purchase_invoice_supplier_map() pi_supplier_map = get_purchase_invoice_supplier_map()
@@ -125,7 +125,9 @@ def get_data(filters):
"available_for_use_date", "status", "purchase_invoice"]) "available_for_use_date", "status", "purchase_invoice"])
for asset in assets_record: for asset in assets_record:
if current_value_map.get(asset.name) is not None: asset_value = asset.gross_purchase_amount - flt(asset.opening_accumulated_depreciation) \
- flt(depreciation_amount_map.get(asset.name))
if asset_value:
row = { row = {
"asset_id": asset.name, "asset_id": asset.name,
"asset_name": asset.asset_name, "asset_name": asset.asset_name,
@@ -138,19 +140,24 @@ def get_data(filters):
"location": asset.location, "location": asset.location,
"asset_category": asset.asset_category, "asset_category": asset.asset_category,
"purchase_date": asset.purchase_date, "purchase_date": asset.purchase_date,
"current_value": current_value_map.get(asset.name) "asset_value": asset_value
} }
data.append(row) data.append(row)
return data return data
def get_finance_book_value_map(finance_book=''): def get_finance_book_value_map(date, finance_book=''):
if not date:
date = today()
return frappe._dict(frappe.db.sql(''' Select return frappe._dict(frappe.db.sql(''' Select
parent, value_after_depreciation parent, SUM(depreciation_amount)
FROM `tabAsset Finance Book` FROM `tabDepreciation Schedule`
WHERE WHERE
parentfield='finance_books' parentfield='schedules'
AND ifnull(finance_book, '')=%s''', cstr(finance_book))) AND schedule_date<=%s
AND journal_entry IS NOT NULL
AND ifnull(finance_book, '')=%s
GROUP BY parent''', (date, cstr(finance_book))))
def get_purchase_receipt_supplier_map(): def get_purchase_receipt_supplier_map():
return frappe._dict(frappe.db.sql(''' Select return frappe._dict(frappe.db.sql(''' Select

View File

@@ -18,6 +18,7 @@ frappe.ui.form.on("Purchase Order", {
return { return {
filters: { filters: {
"company": frm.doc.company, "company": frm.doc.company,
"name": ['!=', frm.doc.supplier_warehouse],
"is_group": 0 "is_group": 0
} }
} }
@@ -283,6 +284,8 @@ erpnext.buying.PurchaseOrderController = erpnext.buying.BuyingController.extend(
}) })
} }
me.dialog.get_field('sub_con_rm_items').check_all_rows()
me.dialog.show() me.dialog.show()
this.dialog.set_primary_action(__('Transfer'), function() { this.dialog.set_primary_action(__('Transfer'), function() {
me.values = me.dialog.get_values(); me.values = me.dialog.get_values();

View File

@@ -0,0 +1,3 @@
{% include "erpnext/regional/india/taxes.js" %}
erpnext.setup_auto_gst_taxation('Purchase Order');

View File

@@ -519,47 +519,62 @@ class TestPurchaseOrder(unittest.TestCase):
def test_backflush_based_on_stock_entry(self): def test_backflush_based_on_stock_entry(self):
item_code = "_Test Subcontracted FG Item 1" item_code = "_Test Subcontracted FG Item 1"
make_subcontracted_item(item_code) make_subcontracted_item(item_code)
make_item('Sub Contracted Raw Material 1', {
'is_stock_item': 1,
'is_sub_contracted_item': 1
})
update_backflush_based_on("Material Transferred for Subcontract") update_backflush_based_on("Material Transferred for Subcontract")
po = create_purchase_order(item_code=item_code, qty=1,
order_qty = 5
po = create_purchase_order(item_code=item_code, qty=order_qty,
is_subcontracted="Yes", supplier_warehouse="_Test Warehouse 1 - _TC") is_subcontracted="Yes", supplier_warehouse="_Test Warehouse 1 - _TC")
make_stock_entry(target="_Test Warehouse - _TC", qty=10, basic_rate=100)
make_stock_entry(target="_Test Warehouse - _TC", make_stock_entry(target="_Test Warehouse - _TC",
item_code="_Test Item Home Desktop 100", qty=10, basic_rate=100) item_code="_Test Item Home Desktop 100", qty=10, basic_rate=100)
make_stock_entry(target="_Test Warehouse - _TC", make_stock_entry(target="_Test Warehouse - _TC",
item_code = "Test Extra Item 1", qty=100, basic_rate=100) item_code = "Test Extra Item 1", qty=100, basic_rate=100)
make_stock_entry(target="_Test Warehouse - _TC", make_stock_entry(target="_Test Warehouse - _TC",
item_code = "Test Extra Item 2", qty=10, basic_rate=100) item_code = "Test Extra Item 2", qty=10, basic_rate=100)
make_stock_entry(target="_Test Warehouse - _TC",
item_code = "Sub Contracted Raw Material 1", qty=10, basic_rate=100)
rm_item = [ rm_items = [
{"item_code":item_code,"rm_item_code":"_Test Item","item_name":"_Test Item", {"item_code":item_code,"rm_item_code":"Sub Contracted Raw Material 1","item_name":"_Test Item",
"qty":1,"warehouse":"_Test Warehouse - _TC","rate":100,"amount":100,"stock_uom":"Nos"}, "qty":10,"warehouse":"_Test Warehouse - _TC", "stock_uom":"Nos"},
{"item_code":item_code,"rm_item_code":"_Test Item Home Desktop 100","item_name":"_Test Item Home Desktop 100", {"item_code":item_code,"rm_item_code":"_Test Item Home Desktop 100","item_name":"_Test Item Home Desktop 100",
"qty":2,"warehouse":"_Test Warehouse - _TC","rate":100,"amount":200,"stock_uom":"Nos"}, "qty":20,"warehouse":"_Test Warehouse - _TC", "stock_uom":"Nos"},
{"item_code":item_code,"rm_item_code":"Test Extra Item 1","item_name":"Test Extra Item 1", {"item_code":item_code,"rm_item_code":"Test Extra Item 1","item_name":"Test Extra Item 1",
"qty":1,"warehouse":"_Test Warehouse - _TC","rate":100,"amount":200,"stock_uom":"Nos"}] "qty":10,"warehouse":"_Test Warehouse - _TC", "stock_uom":"Nos"},
{'item_code': item_code, 'rm_item_code': 'Test Extra Item 2', 'stock_uom':'Nos',
'qty': 10, 'warehouse': '_Test Warehouse - _TC', 'item_name':'Test Extra Item 2'}]
rm_item_string = json.dumps(rm_item) rm_item_string = json.dumps(rm_items)
se = frappe.get_doc(make_subcontract_transfer_entry(po.name, rm_item_string)) se = frappe.get_doc(make_subcontract_transfer_entry(po.name, rm_item_string))
se.append('items', {
'item_code': "Test Extra Item 2",
"qty": 1,
"rate": 100,
"s_warehouse": "_Test Warehouse - _TC",
"t_warehouse": "_Test Warehouse 1 - _TC"
})
se.set_missing_values()
se.submit() se.submit()
pr = make_purchase_receipt(po.name) pr = make_purchase_receipt(po.name)
received_qty = 2
# partial receipt
pr.get('items')[0].qty = received_qty
pr.save() pr.save()
pr.submit() pr.submit()
se_items = sorted([d.item_code for d in se.get('items')]) transferred_items = sorted([d.item_code for d in se.get('items') if se.purchase_order == po.name])
supplied_items = sorted([d.rm_item_code for d in pr.get('supplied_items')]) issued_items = sorted([d.rm_item_code for d in pr.get('supplied_items')])
self.assertEquals(transferred_items, issued_items)
self.assertEquals(pr.get('items')[0].rm_supp_cost, 2000)
transferred_rm_map = frappe._dict()
for item in rm_items:
transferred_rm_map[item.get('rm_item_code')] = item
for item in pr.get('supplied_items'):
self.assertEqual(item.get('required_qty'), (transferred_rm_map[item.get('rm_item_code')].get('qty') / order_qty) * received_qty)
self.assertEquals(se_items, supplied_items)
update_backflush_based_on("BOM") update_backflush_based_on("BOM")
def test_advance_payment_entry_unlink_against_purchase_order(self): def test_advance_payment_entry_unlink_against_purchase_order(self):

View File

@@ -1,537 +1,168 @@
{ {
"allow_copy": 0, "creation": "2013-02-22 01:27:42",
"allow_events_in_timeline": 0, "doctype": "DocType",
"allow_guest_to_view": 0, "editable_grid": 1,
"allow_import": 0, "engine": "InnoDB",
"allow_rename": 0, "field_order": [
"beta": 0, "main_item_code",
"creation": "2013-02-22 01:27:42", "rm_item_code",
"custom": 0, "description",
"docstatus": 0, "batch_no",
"doctype": "DocType", "serial_no",
"editable_grid": 1, "col_break1",
"engine": "InnoDB", "required_qty",
"consumed_qty",
"stock_uom",
"rate",
"amount",
"conversion_factor",
"current_stock",
"reference_name",
"bom_detail_no"
],
"fields": [ "fields": [
{ {
"allow_bulk_edit": 0, "fieldname": "main_item_code",
"allow_in_quick_entry": 0, "fieldtype": "Link",
"allow_on_submit": 0, "in_list_view": 1,
"bold": 0, "label": "Item Code",
"collapsible": 0, "oldfieldname": "main_item_code",
"columns": 0, "oldfieldtype": "Data",
"fieldname": "main_item_code", "options": "Item",
"fieldtype": "Link", "read_only": 1
"hidden": 0, },
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Item Code",
"length": 0,
"no_copy": 0,
"oldfieldname": "main_item_code",
"oldfieldtype": "Data",
"options": "Item",
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 1,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{ {
"allow_bulk_edit": 0, "fieldname": "rm_item_code",
"allow_in_quick_entry": 0, "fieldtype": "Link",
"allow_on_submit": 0, "in_list_view": 1,
"bold": 0, "label": "Raw Material Item Code",
"collapsible": 0, "oldfieldname": "rm_item_code",
"columns": 0, "oldfieldtype": "Data",
"fieldname": "rm_item_code", "options": "Item",
"fieldtype": "Link", "read_only": 1
"hidden": 0, },
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Raw Material Item Code",
"length": 0,
"no_copy": 0,
"oldfieldname": "rm_item_code",
"oldfieldtype": "Data",
"options": "Item",
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 1,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{ {
"allow_bulk_edit": 0, "fieldname": "description",
"allow_in_quick_entry": 0, "fieldtype": "Text Editor",
"allow_on_submit": 0, "in_global_search": 1,
"bold": 0, "in_list_view": 1,
"collapsible": 0, "label": "Description",
"columns": 0, "oldfieldname": "description",
"fieldname": "description", "oldfieldtype": "Data",
"fieldtype": "Text Editor", "print_width": "300px",
"hidden": 0, "read_only": 1,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 1,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Description",
"length": 0,
"no_copy": 0,
"oldfieldname": "description",
"oldfieldtype": "Data",
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"print_width": "300px",
"read_only": 1,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0,
"width": "300px" "width": "300px"
}, },
{ {
"allow_bulk_edit": 0, "fieldname": "batch_no",
"allow_in_quick_entry": 0, "fieldtype": "Link",
"allow_on_submit": 0, "label": "Batch No",
"bold": 0, "no_copy": 1,
"collapsible": 0, "options": "Batch"
"columns": 0, },
"fieldname": "batch_no",
"fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Batch No",
"length": 0,
"no_copy": 1,
"options": "Batch",
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{ {
"allow_bulk_edit": 0, "fieldname": "serial_no",
"allow_in_quick_entry": 0, "fieldtype": "Text",
"allow_on_submit": 0, "label": "Serial No",
"bold": 0, "no_copy": 1
"collapsible": 0, },
"columns": 0,
"fieldname": "serial_no",
"fieldtype": "Text",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Serial No",
"length": 0,
"no_copy": 1,
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{ {
"allow_bulk_edit": 0, "fieldname": "col_break1",
"allow_in_quick_entry": 0, "fieldtype": "Column Break"
"allow_on_submit": 0, },
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "col_break1",
"fieldtype": "Column Break",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"length": 0,
"no_copy": 0,
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{ {
"allow_bulk_edit": 0, "fieldname": "required_qty",
"allow_in_quick_entry": 0, "fieldtype": "Float",
"allow_on_submit": 0, "in_list_view": 1,
"bold": 0, "label": "Required Qty",
"collapsible": 0, "oldfieldname": "required_qty",
"columns": 0, "oldfieldtype": "Currency",
"fieldname": "required_qty", "read_only": 1
"fieldtype": "Float", },
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Required Qty",
"length": 0,
"no_copy": 0,
"oldfieldname": "required_qty",
"oldfieldtype": "Currency",
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 1,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{ {
"allow_bulk_edit": 0, "fieldname": "consumed_qty",
"allow_in_quick_entry": 0, "fieldtype": "Float",
"allow_on_submit": 0, "label": "Consumed Qty",
"bold": 0, "oldfieldname": "consumed_qty",
"collapsible": 0, "oldfieldtype": "Currency",
"columns": 0, "read_only": 1,
"fieldname": "consumed_qty", "reqd": 1
"fieldtype": "Float", },
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Consumed Qty",
"length": 0,
"no_copy": 0,
"oldfieldname": "consumed_qty",
"oldfieldtype": "Currency",
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 1,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{ {
"allow_bulk_edit": 0, "fieldname": "stock_uom",
"allow_in_quick_entry": 0, "fieldtype": "Link",
"allow_on_submit": 0, "label": "Stock Uom",
"bold": 0, "oldfieldname": "stock_uom",
"collapsible": 0, "oldfieldtype": "Data",
"columns": 0, "options": "UOM",
"fieldname": "stock_uom", "read_only": 1
"fieldtype": "Link", },
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Stock Uom",
"length": 0,
"no_copy": 0,
"oldfieldname": "stock_uom",
"oldfieldtype": "Data",
"options": "UOM",
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 1,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{ {
"allow_bulk_edit": 0, "fieldname": "rate",
"allow_in_quick_entry": 0, "fieldtype": "Currency",
"allow_on_submit": 0, "label": "Rate",
"bold": 0, "oldfieldname": "rate",
"collapsible": 0, "oldfieldtype": "Currency",
"columns": 0, "options": "Company:company:default_currency",
"fieldname": "rate", "read_only": 1
"fieldtype": "Currency", },
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Rate",
"length": 0,
"no_copy": 0,
"oldfieldname": "rate",
"oldfieldtype": "Currency",
"options": "Company:company:default_currency",
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 1,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{ {
"allow_bulk_edit": 0, "fieldname": "amount",
"allow_in_quick_entry": 0, "fieldtype": "Currency",
"allow_on_submit": 0, "label": "Amount",
"bold": 0, "oldfieldname": "amount",
"collapsible": 0, "oldfieldtype": "Currency",
"columns": 0, "options": "Company:company:default_currency",
"fieldname": "amount", "read_only": 1
"fieldtype": "Currency", },
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Amount",
"length": 0,
"no_copy": 0,
"oldfieldname": "amount",
"oldfieldtype": "Currency",
"options": "Company:company:default_currency",
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 1,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{ {
"allow_bulk_edit": 0, "fieldname": "conversion_factor",
"allow_in_quick_entry": 0, "fieldtype": "Float",
"allow_on_submit": 0, "hidden": 1,
"bold": 0, "label": "Conversion Factor",
"collapsible": 0, "oldfieldname": "conversion_factor",
"columns": 0, "oldfieldtype": "Currency",
"fieldname": "conversion_factor", "read_only": 1
"fieldtype": "Float", },
"hidden": 1,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Conversion Factor",
"length": 0,
"no_copy": 0,
"oldfieldname": "conversion_factor",
"oldfieldtype": "Currency",
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 1,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{ {
"allow_bulk_edit": 0, "fieldname": "current_stock",
"allow_in_quick_entry": 0, "fieldtype": "Float",
"allow_on_submit": 0, "in_list_view": 1,
"bold": 0, "label": "Current Stock",
"collapsible": 0, "oldfieldname": "current_stock",
"columns": 0, "oldfieldtype": "Currency",
"fieldname": "current_stock", "read_only": 1
"fieldtype": "Float", },
"hidden": 1,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Current Stock",
"length": 0,
"no_copy": 0,
"oldfieldname": "current_stock",
"oldfieldtype": "Currency",
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 1,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{ {
"allow_bulk_edit": 0, "fieldname": "reference_name",
"allow_in_quick_entry": 0, "fieldtype": "Data",
"allow_on_submit": 0, "hidden": 1,
"bold": 0, "in_list_view": 1,
"collapsible": 0, "label": "Reference Name",
"columns": 0, "oldfieldname": "reference_name",
"fieldname": "reference_name", "oldfieldtype": "Data",
"fieldtype": "Data", "read_only": 1
"hidden": 1, },
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Reference Name",
"length": 0,
"no_copy": 0,
"oldfieldname": "reference_name",
"oldfieldtype": "Data",
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 1,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{ {
"allow_bulk_edit": 0, "fieldname": "bom_detail_no",
"allow_in_quick_entry": 0, "fieldtype": "Data",
"allow_on_submit": 0, "hidden": 1,
"bold": 0, "in_list_view": 1,
"collapsible": 0, "label": "BOM Detail No",
"columns": 0, "oldfieldname": "bom_detail_no",
"fieldname": "bom_detail_no", "oldfieldtype": "Data",
"fieldtype": "Data", "read_only": 1
"hidden": 1,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "BOM Detail No",
"length": 0,
"no_copy": 0,
"oldfieldname": "bom_detail_no",
"oldfieldtype": "Data",
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 1,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
} }
], ],
"has_web_view": 0, "idx": 1,
"hide_heading": 0, "istable": 1,
"hide_toolbar": 0, "modified": "2019-11-21 16:25:29.909112",
"idx": 1, "modified_by": "Administrator",
"image_view": 0, "module": "Buying",
"in_create": 0, "name": "Purchase Receipt Item Supplied",
"is_submittable": 0, "owner": "wasim@webnotestech.com",
"issingle": 0, "permissions": [],
"istable": 1, "sort_field": "modified",
"max_attachments": 0, "sort_order": "DESC",
"modified": "2019-01-07 16:51:59.536291", "track_changes": 1
"modified_by": "Administrator",
"module": "Buying",
"name": "Purchase Receipt Item Supplied",
"owner": "wasim@webnotestech.com",
"permissions": [],
"quick_entry": 0,
"read_only": 0,
"read_only_onload": 0,
"show_name_in_global_search": 0,
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1,
"track_seen": 0,
"track_views": 0
} }

View File

@@ -0,0 +1,33 @@
# Version 12.3.0 Release Notes
### Accounting
1. Statewise GST taxation for India
- Added GST state in the tax category
- Added tax category in the address, sales/purchase tax template
- Based on the address system will fetch the tax template
2. Accounts Payable report based on payment terms
3. Trial Balance Report with filter "Party Name"
4. Fixed asset register report with date filters
### CRM
1. Appointment Scheduling
- Configure the appointment slots using Appointment Booking Settings
- Users can book the appointment through the portal based on slot availability
### HR
1. Refactored Employee Attendance Tool
2. Set allocated amount in employee advance as per total amount
### Fixes
1. Stock entry decimal issue while creating the GL entries
2. Item wise stock balance report
3. Valuation of subcontracting finished good item
4. Not able to create Instructor, Student entries
5. Pricing rule for a product discount
6. POS for serialized items
7. Not able to cancel share transfer entry
8. Ledger entries for compensatory off were not getting created

View File

@@ -46,6 +46,16 @@ def get_data():
"name": "Contract", "name": "Contract",
"description": _("Helps you keep tracks of Contracts based on Supplier, Customer and Employee"), "description": _("Helps you keep tracks of Contracts based on Supplier, Customer and Employee"),
}, },
{
"type": "doctype",
"name": "Appointment",
"description" : _("Helps you manage appointments with your leads"),
},
{
"type": "doctype",
"name": "Newsletter",
"label": _("Newsletter"),
}
] ]
}, },
{ {
@@ -165,6 +175,11 @@ def get_data():
"type": "doctype", "type": "doctype",
"name": "SMS Settings", "name": "SMS Settings",
"description": _("Setup SMS gateway settings") "description": _("Setup SMS gateway settings")
},
{
"type": "doctype",
"label": _("Email Group"),
"name": "Email Group",
} }
] ]
}, },

View File

@@ -241,6 +241,10 @@ def get_data():
"type": "doctype", "type": "doctype",
"name": "Quality Inspection Template", "name": "Quality Inspection Template",
}, },
{
"type": "doctype",
"name": "Quick Stock Balance",
},
] ]
}, },
{ {

View File

@@ -5,15 +5,17 @@ from __future__ import unicode_literals
import frappe, erpnext import frappe, erpnext
import json import json
from frappe import _, throw from frappe import _, throw
from frappe.utils import today, flt, cint, fmt_money, formatdate, getdate, add_days, add_months, get_last_day, nowdate from frappe.utils import (today, flt, cint, fmt_money, formatdate,
from erpnext.stock.get_item_details import get_conversion_factor getdate, add_days, add_months, get_last_day, nowdate, get_link_to_form)
from erpnext.stock.get_item_details import get_conversion_factor, get_item_details
from erpnext.setup.utils import get_exchange_rate from erpnext.setup.utils import get_exchange_rate
from erpnext.accounts.utils import get_fiscal_years, validate_fiscal_year, get_account_currency from erpnext.accounts.utils import get_fiscal_years, validate_fiscal_year, get_account_currency
from erpnext.utilities.transaction_base import TransactionBase from erpnext.utilities.transaction_base import TransactionBase
from erpnext.buying.utils import update_last_purchase_rate from erpnext.buying.utils import update_last_purchase_rate
from erpnext.controllers.sales_and_purchase_return import validate_return from erpnext.controllers.sales_and_purchase_return import validate_return
from erpnext.accounts.party import get_party_account_currency, validate_party_frozen_disabled from erpnext.accounts.party import get_party_account_currency, validate_party_frozen_disabled
from erpnext.accounts.doctype.pricing_rule.utils import validate_pricing_rules from erpnext.accounts.doctype.pricing_rule.utils import (apply_pricing_rule_on_transaction,
apply_pricing_rule_for_free_items, get_applied_pricing_rules)
from erpnext.exceptions import InvalidCurrency from erpnext.exceptions import InvalidCurrency
from six import text_type from six import text_type
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import get_accounting_dimensions from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import get_accounting_dimensions
@@ -59,7 +61,6 @@ class AccountsController(TransactionBase):
_('{0} is blocked so this transaction cannot proceed'.format(supplier_name)), raise_exception=1) _('{0} is blocked so this transaction cannot proceed'.format(supplier_name)), raise_exception=1)
def validate(self): def validate(self):
if not self.get('is_return'): if not self.get('is_return'):
self.validate_qty_is_not_zero() self.validate_qty_is_not_zero()
@@ -98,10 +99,22 @@ class AccountsController(TransactionBase):
if self.is_return: if self.is_return:
self.validate_qty() self.validate_qty()
else:
self.validate_deferred_start_and_end_date()
validate_regional(self) validate_regional(self)
if self.doctype != 'Material Request': if self.doctype != 'Material Request':
validate_pricing_rules(self) apply_pricing_rule_on_transaction(self)
def validate_deferred_start_and_end_date(self):
for d in self.items:
if d.get("enable_deferred_revenue") or d.get("enable_deferred_expense"):
if not (d.service_start_date and d.service_end_date):
frappe.throw(_("Row #{0}: Service Start and End Date is required for deferred accounting").format(d.idx))
elif getdate(d.service_start_date) > getdate(d.service_end_date):
frappe.throw(_("Row #{0}: Service Start Date cannot be greater than Service End Date").format(d.idx))
elif getdate(self.posting_date) > getdate(d.service_end_date):
frappe.throw(_("Row #{0}: Service End Date cannot be before Invoice Posting Date").format(d.idx))
def validate_invoice_documents_schedule(self): def validate_invoice_documents_schedule(self):
self.validate_payment_schedule_dates() self.validate_payment_schedule_dates()
@@ -232,7 +245,6 @@ class AccountsController(TransactionBase):
def set_missing_item_details(self, for_validate=False): def set_missing_item_details(self, for_validate=False):
"""set missing item values""" """set missing item values"""
from erpnext.stock.get_item_details import get_item_details
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
if hasattr(self, "items"): if hasattr(self, "items"):
@@ -244,7 +256,6 @@ class AccountsController(TransactionBase):
document_type = "{} Item".format(self.doctype) document_type = "{} Item".format(self.doctype)
parent_dict.update({"document_type": document_type}) parent_dict.update({"document_type": document_type})
self.set('pricing_rules', [])
# party_name field used for customer in quotation # party_name field used for customer in quotation
if self.doctype == "Quotation" and self.quotation_to == "Customer" and parent_dict.get("party_name"): if self.doctype == "Quotation" and self.quotation_to == "Customer" and parent_dict.get("party_name"):
parent_dict.update({"customer": parent_dict.get("party_name")}) parent_dict.update({"customer": parent_dict.get("party_name")})
@@ -264,7 +275,7 @@ class AccountsController(TransactionBase):
if self.get("is_subcontracted"): if self.get("is_subcontracted"):
args["is_subcontracted"] = self.is_subcontracted args["is_subcontracted"] = self.is_subcontracted
ret = get_item_details(args, self, overwrite_warehouse=False) ret = get_item_details(args, self, for_validate=True, overwrite_warehouse=False)
for fieldname, value in ret.items(): for fieldname, value in ret.items():
if item.meta.get_field(fieldname) and value is not None: if item.meta.get_field(fieldname) and value is not None:
@@ -285,24 +296,42 @@ class AccountsController(TransactionBase):
if self.doctype in ["Purchase Invoice", "Sales Invoice"] and item.meta.get_field('is_fixed_asset'): if self.doctype in ["Purchase Invoice", "Sales Invoice"] and item.meta.get_field('is_fixed_asset'):
item.set('is_fixed_asset', ret.get('is_fixed_asset', 0)) item.set('is_fixed_asset', ret.get('is_fixed_asset', 0))
if ret.get("pricing_rules") and not ret.get("validate_applied_rule", 0): if ret.get("pricing_rules"):
# if user changed the discount percentage then set user's discount percentage ? self.apply_pricing_rule_on_items(item, ret)
item.set("pricing_rules", ret.get("pricing_rules"))
item.set("discount_percentage", ret.get("discount_percentage"))
item.set("discount_amount", ret.get("discount_amount"))
if ret.get("pricing_rule_for") == "Rate":
item.set("price_list_rate", ret.get("price_list_rate"))
if item.get("price_list_rate"):
item.rate = flt(item.price_list_rate *
(1.0 - (flt(item.discount_percentage) / 100.0)), item.precision("rate"))
if item.get('discount_amount'):
item.rate = item.price_list_rate - item.discount_amount
if self.doctype == "Purchase Invoice": if self.doctype == "Purchase Invoice":
self.set_expense_account(for_validate) self.set_expense_account(for_validate)
def apply_pricing_rule_on_items(self, item, pricing_rule_args):
if not pricing_rule_args.get("validate_applied_rule", 0):
# if user changed the discount percentage then set user's discount percentage ?
if pricing_rule_args.get("price_or_product_discount") == 'Price':
item.set("pricing_rules", pricing_rule_args.get("pricing_rules"))
item.set("discount_percentage", pricing_rule_args.get("discount_percentage"))
item.set("discount_amount", pricing_rule_args.get("discount_amount"))
if pricing_rule_args.get("pricing_rule_for") == "Rate":
item.set("price_list_rate", pricing_rule_args.get("price_list_rate"))
if item.get("price_list_rate"):
item.rate = flt(item.price_list_rate *
(1.0 - (flt(item.discount_percentage) / 100.0)), item.precision("rate"))
if item.get('discount_amount'):
item.rate = item.price_list_rate - item.discount_amount
elif pricing_rule_args.get('free_item_data'):
apply_pricing_rule_for_free_items(self, pricing_rule_args.get('free_item_data'))
elif pricing_rule_args.get("validate_applied_rule"):
for pricing_rule in get_applied_pricing_rules(item):
pricing_rule_doc = frappe.get_cached_doc("Pricing Rule", pricing_rule)
for field in ['discount_percentage', 'discount_amount', 'rate']:
if item.get(field) < pricing_rule_doc.get(field):
title = get_link_to_form("Pricing Rule", pricing_rule)
frappe.msgprint(_("Row {0}: user has not applied the rule {1} on the item {2}")
.format(item.idx, frappe.bold(title), frappe.bold(item.item_code)))
def set_taxes(self): def set_taxes(self):
if not self.meta.get_field("taxes"): if not self.meta.get_field("taxes"):
return return
@@ -397,9 +426,10 @@ class AccountsController(TransactionBase):
return gl_dict return gl_dict
def validate_qty_is_not_zero(self): def validate_qty_is_not_zero(self):
for item in self.items: if self.doctype != "Purchase Receipt":
if not item.qty: for item in self.items:
frappe.throw(_("Item quantity can not be zero")) if not item.qty:
frappe.throw(_("Item quantity can not be zero"))
def validate_account_currency(self, account, account_currency=None): def validate_account_currency(self, account, account_currency=None):
valid_currency = [self.company_currency] valid_currency = [self.company_currency]

View File

@@ -221,7 +221,7 @@ class BuyingController(StockController):
"backflush_raw_materials_of_subcontract_based_on") "backflush_raw_materials_of_subcontract_based_on")
if (self.doctype == 'Purchase Receipt' and if (self.doctype == 'Purchase Receipt' and
backflush_raw_materials_based_on != 'BOM'): backflush_raw_materials_based_on != 'BOM'):
self.update_raw_materials_supplied_based_on_stock_entries(raw_material_table) self.update_raw_materials_supplied_based_on_stock_entries()
else: else:
for item in self.get("items"): for item in self.get("items"):
if self.doctype in ["Purchase Receipt", "Purchase Invoice"]: if self.doctype in ["Purchase Receipt", "Purchase Invoice"]:
@@ -241,41 +241,96 @@ class BuyingController(StockController):
if self.is_subcontracted == "No" and self.get("supplied_items"): if self.is_subcontracted == "No" and self.get("supplied_items"):
self.set('supplied_items', []) self.set('supplied_items', [])
def update_raw_materials_supplied_based_on_stock_entries(self, raw_material_table): def update_raw_materials_supplied_based_on_stock_entries(self):
self.set(raw_material_table, []) self.set('supplied_items', [])
purchase_orders = [d.purchase_order for d in self.items]
if purchase_orders:
items = get_subcontracted_raw_materials_from_se(purchase_orders)
backflushed_raw_materials = get_backflushed_subcontracted_raw_materials_from_se(purchase_orders, self.name)
for d in items: purchase_orders = set([d.purchase_order for d in self.items])
qty = d.qty - backflushed_raw_materials.get(d.item_code, 0)
rm = self.append(raw_material_table, {})
rm.rm_item_code = d.item_code
rm.item_name = d.item_name
rm.main_item_code = d.main_item_code
rm.description = d.description
rm.stock_uom = d.stock_uom
rm.required_qty = qty
rm.consumed_qty = qty
rm.serial_no = d.serial_no
rm.batch_no = d.batch_no
# get raw materials rate # qty of raw materials backflushed (for each item per purchase order)
from erpnext.stock.utils import get_incoming_rate backflushed_raw_materials_map = get_backflushed_subcontracted_raw_materials(purchase_orders)
rm.rate = get_incoming_rate({
"item_code": d.item_code,
"warehouse": self.supplier_warehouse,
"posting_date": self.posting_date,
"posting_time": self.posting_time,
"qty": -1 * qty,
"serial_no": rm.serial_no
})
if not rm.rate:
rm.rate = get_valuation_rate(d.item_code, self.supplier_warehouse,
self.doctype, self.name, currency=self.company_currency, company = self.company)
rm.amount = qty * flt(rm.rate) # qty of "finished good" item yet to be received
qty_to_be_received_map = get_qty_to_be_received(purchase_orders)
for item in self.get('items'):
# reset raw_material cost
item.rm_supp_cost = 0
# qty of raw materials transferred to the supplier
transferred_raw_materials = get_subcontracted_raw_materials_from_se(item.purchase_order, item.item_code)
non_stock_items = get_non_stock_items(item.purchase_order, item.item_code)
item_key = '{}{}'.format(item.item_code, item.purchase_order)
fg_yet_to_be_received = qty_to_be_received_map.get(item_key)
transferred_batch_qty_map = get_transferred_batch_qty_map(item.purchase_order, item.item_code)
backflushed_batch_qty_map = get_backflushed_batch_qty_map(item.purchase_order, item.item_code)
for raw_material in transferred_raw_materials + non_stock_items:
rm_item_key = '{}{}'.format(raw_material.rm_item_code, item.purchase_order)
raw_material_data = backflushed_raw_materials_map.get(rm_item_key, {})
consumed_qty = raw_material_data.get('qty', 0)
consumed_serial_nos = raw_material_data.get('serial_nos', '')
consumed_batch_nos = raw_material_data.get('batch_nos', '')
transferred_qty = raw_material.qty
rm_qty_to_be_consumed = transferred_qty - consumed_qty
# backflush all remaining transferred qty in the last Purchase Receipt
if fg_yet_to_be_received == item.qty:
qty = rm_qty_to_be_consumed
else:
qty = (rm_qty_to_be_consumed / fg_yet_to_be_received) * item.qty
if frappe.get_cached_value('UOM', raw_material.stock_uom, 'must_be_whole_number'):
qty = frappe.utils.ceil(qty)
if qty > rm_qty_to_be_consumed:
qty = rm_qty_to_be_consumed
if not qty: continue
if raw_material.serial_nos:
set_serial_nos(raw_material, consumed_serial_nos, qty)
if raw_material.batch_nos:
batches_qty = get_batches_with_qty(raw_material.rm_item_code, raw_material.main_item_code,
qty, transferred_batch_qty_map, backflushed_batch_qty_map)
for batch_data in batches_qty:
qty = batch_data['qty']
raw_material.batch_no = batch_data['batch']
self.append_raw_material_to_be_backflushed(item, raw_material, qty)
else:
self.append_raw_material_to_be_backflushed(item, raw_material, qty)
def append_raw_material_to_be_backflushed(self, fg_item_doc, raw_material_data, qty):
rm = self.append('supplied_items', {})
rm.update(raw_material_data)
rm.required_qty = qty
rm.consumed_qty = qty
if not raw_material_data.get('non_stock_item'):
from erpnext.stock.utils import get_incoming_rate
rm.rate = get_incoming_rate({
"item_code": raw_material_data.rm_item_code,
"warehouse": self.supplier_warehouse,
"posting_date": self.posting_date,
"posting_time": self.posting_time,
"qty": -1 * qty,
"serial_no": rm.serial_no
})
if not rm.rate:
rm.rate = get_valuation_rate(raw_material_data.item_code, self.supplier_warehouse,
self.doctype, self.name, currency=self.company_currency, company=self.company)
rm.amount = qty * flt(rm.rate)
fg_item_doc.rm_supp_cost += rm.amount
def update_raw_materials_supplied_based_on_bom(self, item, raw_material_table): def update_raw_materials_supplied_based_on_bom(self, item, raw_material_table):
exploded_item = 1 exploded_item = 1
@@ -387,9 +442,11 @@ class BuyingController(StockController):
item_codes = list(set(item.item_code for item in item_codes = list(set(item.item_code for item in
self.get("items"))) self.get("items")))
if item_codes: if item_codes:
self._sub_contracted_items = [r[0] for r in frappe.db.sql("""select name items = frappe.get_all('Item', filters={
from `tabItem` where name in (%s) and is_sub_contracted_item=1""" % \ 'name': ['in', item_codes],
(", ".join((["%s"]*len(item_codes))),), item_codes)] 'is_sub_contracted_item': 1
})
self._sub_contracted_items = [item.name for item in items]
return self._sub_contracted_items return self._sub_contracted_items
@@ -722,28 +779,72 @@ def get_items_from_bom(item_code, bom, exploded_item=1):
return bom_items return bom_items
def get_subcontracted_raw_materials_from_se(purchase_orders): def get_subcontracted_raw_materials_from_se(purchase_order, fg_item):
return frappe.db.sql(""" common_query = """
select SELECT
sed.item_name, sed.item_code, sum(sed.qty) as qty, sed.description, sed.item_code AS rm_item_code,
sed.stock_uom, sed.subcontracted_item as main_item_code, sed.serial_no, sed.batch_no SUM(sed.qty) AS qty,
from `tabStock Entry` se,`tabStock Entry Detail` sed sed.description,
where sed.stock_uom,
se.name = sed.parent and se.docstatus=1 and se.purpose='Send to Subcontractor' sed.subcontracted_item AS main_item_code,
and se.purchase_order in (%s) and ifnull(sed.t_warehouse, '') != '' {serial_no_concat_syntax} AS serial_nos,
group by sed.item_code, sed.t_warehouse {batch_no_concat_syntax} AS batch_nos
""" % (','.join(['%s'] * len(purchase_orders))), tuple(purchase_orders), as_dict=1) FROM `tabStock Entry` se,`tabStock Entry Detail` sed
WHERE
se.name = sed.parent
AND se.docstatus=1
AND se.purpose='Send to Subcontractor'
AND se.purchase_order = %s
AND IFNULL(sed.t_warehouse, '') != ''
AND sed.subcontracted_item = %s
GROUP BY sed.item_code, sed.subcontracted_item
"""
raw_materials = frappe.db.multisql({
'mariadb': common_query.format(
serial_no_concat_syntax="GROUP_CONCAT(sed.serial_no)",
batch_no_concat_syntax="GROUP_CONCAT(sed.batch_no)"
),
'postgres': common_query.format(
serial_no_concat_syntax="STRING_AGG(sed.serial_no, ',')",
batch_no_concat_syntax="STRING_AGG(sed.batch_no, ',')"
)
}, (purchase_order, fg_item), as_dict=1)
def get_backflushed_subcontracted_raw_materials_from_se(purchase_orders, purchase_receipt): return raw_materials
return frappe._dict(frappe.db.sql("""
select def get_backflushed_subcontracted_raw_materials(purchase_orders):
prsi.rm_item_code as item_code, sum(prsi.consumed_qty) as qty common_query = """
from `tabPurchase Receipt` pr, `tabPurchase Receipt Item` pri, `tabPurchase Receipt Item Supplied` prsi SELECT
where CONCAT(prsi.rm_item_code, pri.purchase_order) AS item_key,
pr.name = pri.parent and pr.name = prsi.parent and pri.purchase_order in (%s) SUM(prsi.consumed_qty) AS qty,
and pri.item_code = prsi.main_item_code and pr.name != '%s' and pr.docstatus = 1 {serial_no_concat_syntax} AS serial_nos,
group by prsi.rm_item_code {batch_no_concat_syntax} AS batch_nos
""" % (','.join(['%s'] * len(purchase_orders)), purchase_receipt), tuple(purchase_orders))) FROM `tabPurchase Receipt` pr, `tabPurchase Receipt Item` pri, `tabPurchase Receipt Item Supplied` prsi
WHERE
pr.name = pri.parent
AND pr.name = prsi.parent
AND pri.purchase_order IN %s
AND pri.item_code = prsi.main_item_code
AND pr.docstatus = 1
GROUP BY prsi.rm_item_code, pri.purchase_order
"""
backflushed_raw_materials = frappe.db.multisql({
'mariadb': common_query.format(
serial_no_concat_syntax="GROUP_CONCAT(prsi.serial_no)",
batch_no_concat_syntax="GROUP_CONCAT(prsi.batch_no)"
),
'postgres': common_query.format(
serial_no_concat_syntax="STRING_AGG(prsi.serial_no, ',')",
batch_no_concat_syntax="STRING_AGG(prsi.batch_no, ',')"
)
}, (purchase_orders, ), as_dict=1)
backflushed_raw_materials_map = frappe._dict()
for item in backflushed_raw_materials:
backflushed_raw_materials_map.setdefault(item.item_key, item)
return backflushed_raw_materials_map
def get_asset_item_details(asset_items): def get_asset_item_details(asset_items):
asset_items_data = {} asset_items_data = {}
@@ -776,3 +877,125 @@ def validate_item_type(doc, fieldname, message):
error_message = _("Following item {0} is not marked as {1} item. You can enable them as {1} item from its Item master".format(items, message)) error_message = _("Following item {0} is not marked as {1} item. You can enable them as {1} item from its Item master".format(items, message))
frappe.throw(error_message) frappe.throw(error_message)
def get_qty_to_be_received(purchase_orders):
return frappe._dict(frappe.db.sql("""
SELECT CONCAT(poi.`item_code`, poi.`parent`) AS item_key,
SUM(poi.`qty`) - SUM(poi.`received_qty`) AS qty_to_be_received
FROM `tabPurchase Order Item` poi
WHERE
poi.`parent` in %s
GROUP BY poi.`item_code`, poi.`parent`
HAVING SUM(poi.`qty`) > SUM(poi.`received_qty`)
""", (purchase_orders)))
def get_non_stock_items(purchase_order, fg_item_code):
return frappe.db.sql("""
SELECT
pois.main_item_code,
pois.rm_item_code,
item.description,
pois.required_qty AS qty,
pois.rate,
1 as non_stock_item,
pois.stock_uom
FROM `tabPurchase Order Item Supplied` pois, `tabItem` item
WHERE
pois.`rm_item_code` = item.`name`
AND item.is_stock_item = 0
AND pois.`parent` = %s
AND pois.`main_item_code` = %s
""", (purchase_order, fg_item_code), as_dict=1)
def set_serial_nos(raw_material, consumed_serial_nos, qty):
serial_nos = set(get_serial_nos(raw_material.serial_nos)) - \
set(get_serial_nos(consumed_serial_nos))
if serial_nos and qty <= len(serial_nos):
raw_material.serial_no = '\n'.join(list(serial_nos)[0:frappe.utils.cint(qty)])
def get_transferred_batch_qty_map(purchase_order, fg_item):
# returns
# {
# (item_code, fg_code): {
# batch1: 10, # qty
# batch2: 16
# },
# }
transferred_batch_qty_map = {}
transferred_batches = frappe.db.sql("""
SELECT
sed.batch_no,
SUM(sed.qty) AS qty,
sed.item_code
FROM `tabStock Entry` se,`tabStock Entry Detail` sed
WHERE
se.name = sed.parent
AND se.docstatus=1
AND se.purpose='Send to Subcontractor'
AND se.purchase_order = %s
AND sed.subcontracted_item = %s
AND sed.batch_no IS NOT NULL
GROUP BY
sed.batch_no,
sed.item_code
""", (purchase_order, fg_item), as_dict=1)
for batch_data in transferred_batches:
transferred_batch_qty_map.setdefault((batch_data.item_code, fg_item), {})
transferred_batch_qty_map[(batch_data.item_code, fg_item)][batch_data.batch_no] = batch_data.qty
return transferred_batch_qty_map
def get_backflushed_batch_qty_map(purchase_order, fg_item):
# returns
# {
# (item_code, fg_code): {
# batch1: 10, # qty
# batch2: 16
# },
# }
backflushed_batch_qty_map = {}
backflushed_batches = frappe.db.sql("""
SELECT
pris.batch_no,
SUM(pris.consumed_qty) AS qty,
pris.rm_item_code AS item_code
FROM `tabPurchase Receipt` pr, `tabPurchase Receipt Item` pri, `tabPurchase Receipt Item Supplied` pris
WHERE
pr.name = pri.parent
AND pri.parent = pris.parent
AND pri.purchase_order = %s
AND pri.item_code = pris.main_item_code
AND pr.docstatus = 1
AND pris.main_item_code = %s
AND pris.batch_no IS NOT NULL
GROUP BY
pris.rm_item_code, pris.batch_no
""", (purchase_order, fg_item), as_dict=1)
for batch_data in backflushed_batches:
backflushed_batch_qty_map.setdefault((batch_data.item_code, fg_item), {})
backflushed_batch_qty_map[(batch_data.item_code, fg_item)][batch_data.batch_no] = batch_data.qty
return backflushed_batch_qty_map
def get_batches_with_qty(item_code, fg_item, required_qty, transferred_batch_qty_map, backflushed_batch_qty_map):
# Returns available batches to be backflushed based on requirements
transferred_batches = transferred_batch_qty_map.get((item_code, fg_item), {})
backflushed_batches = backflushed_batch_qty_map.get((item_code, fg_item), {})
available_batches = []
for (batch, transferred_qty) in transferred_batches.items():
backflushed_qty = backflushed_batches.get(batch, 0)
available_qty = transferred_qty - backflushed_qty
if available_qty >= required_qty:
available_batches.append({'batch': batch, 'qty': required_qty})
break
else:
available_batches.append({'batch': batch, 'qty': available_qty})
required_qty -= available_qty
return available_batches

View File

@@ -552,7 +552,7 @@ class calculate_taxes_and_totals(object):
if item.price_list_rate: if item.price_list_rate:
if item.pricing_rules and not self.doc.ignore_pricing_rule: if item.pricing_rules and not self.doc.ignore_pricing_rule:
for d in item.pricing_rules.split(','): for d in item.pricing_rules.split(','):
pricing_rule = frappe.get_doc('Pricing Rule', d) pricing_rule = frappe.get_cached_doc('Pricing Rule', d)
if (pricing_rule.margin_type == 'Amount' and pricing_rule.currency == self.doc.currency)\ if (pricing_rule.margin_type == 'Amount' and pricing_rule.currency == self.doc.currency)\
or (pricing_rule.margin_type == 'Percentage'): or (pricing_rule.margin_type == 'Percentage'):

View File

@@ -0,0 +1,17 @@
// Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
frappe.ui.form.on('Appointment', {
refresh: function(frm) {
if(frm.doc.lead){
frm.add_custom_button(frm.doc.lead,()=>{
frappe.set_route("Form", "Lead", frm.doc.lead);
});
}
if(frm.doc.calendar_event){
frm.add_custom_button(__(frm.doc.calendar_event),()=>{
frappe.set_route("Form", "Event", frm.doc.calendar_event);
});
}
}
});

View File

@@ -0,0 +1,153 @@
{
"autoname": "format:APMT-{customer_name}-{####}",
"creation": "2019-08-27 10:48:27.926283",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"scheduled_time",
"status",
"customer_details_section",
"customer_name",
"customer_phone_number",
"customer_skype",
"customer_email",
"col_br_2",
"customer_details",
"linked_docs_section",
"lead",
"col_br_3",
"calendar_event"
],
"fields": [
{
"fieldname": "customer_details_section",
"fieldtype": "Section Break",
"label": "Customer Details"
},
{
"fieldname": "customer_name",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Name",
"reqd": 1
},
{
"fieldname": "customer_phone_number",
"fieldtype": "Data",
"label": "Phone Number"
},
{
"fieldname": "customer_skype",
"fieldtype": "Data",
"label": "Skype ID"
},
{
"fieldname": "customer_details",
"fieldtype": "Long Text",
"label": "Details"
},
{
"fieldname": "scheduled_time",
"fieldtype": "Datetime",
"in_list_view": 1,
"label": "Scheduled Time",
"reqd": 1
},
{
"fieldname": "status",
"fieldtype": "Select",
"label": "Status",
"options": "Open\nUnverified\nClosed",
"reqd": 1
},
{
"fieldname": "lead",
"fieldtype": "Link",
"label": "Lead",
"options": "Lead"
},
{
"fieldname": "calendar_event",
"fieldtype": "Link",
"label": "Calendar Event",
"options": "Event"
},
{
"fieldname": "col_br_2",
"fieldtype": "Column Break"
},
{
"fieldname": "customer_email",
"fieldtype": "Data",
"label": "Email",
"reqd": 1
},
{
"fieldname": "linked_docs_section",
"fieldtype": "Section Break",
"label": "Linked Documents"
},
{
"fieldname": "col_br_3",
"fieldtype": "Column Break"
}
],
"modified": "2019-10-14 15:23:54.630731",
"modified_by": "Administrator",
"module": "CRM",
"name": "Appointment",
"name_case": "UPPER CASE",
"owner": "Administrator",
"permissions": [
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"share": 1,
"write": 1
},
{
"create": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Guest",
"share": 1
},
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Sales Manager",
"share": 1,
"write": 1
},
{
"create": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Sales User",
"share": 1,
"write": 1
}
],
"quick_entry": 1,
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
}

View File

@@ -0,0 +1,223 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
from __future__ import unicode_literals
import urllib
from collections import Counter
from datetime import timedelta
import frappe
from frappe import _
from frappe.model.document import Document
from frappe.utils import get_url
from frappe.utils.verified_command import verify_request, get_signed_params
class Appointment(Document):
def find_lead_by_email(self):
lead_list = frappe.get_list(
'Lead', filters={'email_id': self.customer_email}, ignore_permissions=True)
if lead_list:
return lead_list[0].name
return None
def before_insert(self):
number_of_appointments_in_same_slot = frappe.db.count(
'Appointment', filters={'scheduled_time': self.scheduled_time})
number_of_agents = frappe.db.get_single_value('Appointment Booking Settings', 'number_of_agents')
if not number_of_agents == 0:
if (number_of_appointments_in_same_slot >= number_of_agents):
frappe.throw('Time slot is not available')
# Link lead
if not self.lead:
self.lead = self.find_lead_by_email()
def after_insert(self):
if self.lead:
# Create Calendar event
self.auto_assign()
self.create_calendar_event()
else:
# Set status to unverified
self.status = 'Unverified'
# Send email to confirm
self.send_confirmation_email()
def send_confirmation_email(self):
verify_url = self._get_verify_url()
template = 'confirm_appointment'
args = {
"link":verify_url,
"site_url":frappe.utils.get_url(),
"full_name":self.customer_name,
}
frappe.sendmail(recipients=[self.customer_email],
template=template,
args=args,
subject=_('Appointment Confirmation'))
if frappe.session.user == "Guest":
frappe.msgprint(
'Please check your email to confirm the appointment')
else :
frappe.msgprint(
'Appointment was created. But no lead was found. Please check the email to confirm')
def on_change(self):
# Sync Calendar
if not self.calendar_event:
return
cal_event = frappe.get_doc('Event', self.calendar_event)
cal_event.starts_on = self.scheduled_time
cal_event.save(ignore_permissions=True)
def set_verified(self, email):
if not email == self.customer_email:
frappe.throw('Email verification failed.')
# Create new lead
self.create_lead_and_link()
# Remove unverified status
self.status = 'Open'
# Create calender event
self.auto_assign()
self.create_calendar_event()
self.save(ignore_permissions=True)
frappe.db.commit()
def create_lead_and_link(self):
# Return if already linked
if self.lead:
return
lead = frappe.get_doc({
'doctype': 'Lead',
'lead_name': self.customer_name,
'email_id': self.customer_email,
'notes': self.customer_details,
'phone': self.customer_phone_number,
})
lead.insert(ignore_permissions=True)
# Link lead
self.lead = lead.name
def auto_assign(self):
from frappe.desk.form.assign_to import add as add_assignemnt
existing_assignee = self.get_assignee_from_latest_opportunity()
if existing_assignee:
# If the latest opportunity is assigned to someone
# Assign the appointment to the same
add_assignemnt({
'doctype': self.doctype,
'name': self.name,
'assign_to': existing_assignee
})
return
if self._assign:
return
available_agents = _get_agents_sorted_by_asc_workload(
self.scheduled_time.date())
for agent in available_agents:
if(_check_agent_availability(agent, self.scheduled_time)):
agent = agent[0]
add_assignemnt({
'doctype': self.doctype,
'name': self.name,
'assign_to': agent
})
break
def get_assignee_from_latest_opportunity(self):
if not self.lead:
return None
if not frappe.db.exists('Lead', self.lead):
return None
opporutnities = frappe.get_list(
'Opportunity',
filters={
'party_name': self.lead,
},
ignore_permissions=True,
order_by='creation desc')
if not opporutnities:
return None
latest_opportunity = frappe.get_doc('Opportunity', opporutnities[0].name )
assignee = latest_opportunity._assign
if not assignee:
return None
assignee = frappe.parse_json(assignee)[0]
return assignee
def create_calendar_event(self):
if self.calendar_event:
return
appointment_event = frappe.get_doc({
'doctype': 'Event',
'subject': ' '.join(['Appointment with', self.customer_name]),
'starts_on': self.scheduled_time,
'status': 'Open',
'type': 'Public',
'send_reminder': frappe.db.get_single_value('Appointment Booking Settings', 'email_reminders'),
'event_participants': [dict(reference_doctype='Lead', reference_docname=self.lead)]
})
employee = _get_employee_from_user(self._assign)
if employee:
appointment_event.append('event_participants', dict(
reference_doctype='Employee',
reference_docname=employee.name))
appointment_event.insert(ignore_permissions=True)
self.calendar_event = appointment_event.name
self.save(ignore_permissions=True)
def _get_verify_url(self):
verify_route = '/book_appointment/verify'
params = {
'email': self.customer_email,
'appointment': self.name
}
return get_url(verify_route + '?' + get_signed_params(params))
def _get_agents_sorted_by_asc_workload(date):
appointments = frappe.db.get_list('Appointment', fields='*')
agent_list = _get_agent_list_as_strings()
if not appointments:
return agent_list
appointment_counter = Counter(agent_list)
for appointment in appointments:
assigned_to = frappe.parse_json(appointment._assign)
if not assigned_to:
continue
if (assigned_to[0] in agent_list) and appointment.scheduled_time.date() == date:
appointment_counter[assigned_to[0]] += 1
sorted_agent_list = appointment_counter.most_common()
sorted_agent_list.reverse()
return sorted_agent_list
def _get_agent_list_as_strings():
agent_list_as_strings = []
agent_list = frappe.get_doc('Appointment Booking Settings').agent_list
for agent in agent_list:
agent_list_as_strings.append(agent.user)
return agent_list_as_strings
def _check_agent_availability(agent_email, scheduled_time):
appointemnts_at_scheduled_time = frappe.get_list(
'Appointment', filters={'scheduled_time': scheduled_time})
for appointment in appointemnts_at_scheduled_time:
if appointment._assign == agent_email:
return False
return True
def _get_employee_from_user(user):
employee_docname = frappe.db.exists(
{'doctype': 'Employee', 'user_id': user})
if employee_docname:
# frappe.db.exists returns a tuple of a tuple
return frappe.get_doc('Employee', employee_docname[0][0])
return None

View File

@@ -0,0 +1,58 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
from __future__ import unicode_literals
import frappe
import unittest
import datetime
def create_test_lead():
test_lead = frappe.db.exists({'doctype': 'Lead', 'lead_name': 'Test Lead'})
if test_lead:
return frappe.get_doc('Lead', test_lead[0][0])
test_lead = frappe.get_doc({
'doctype': 'Lead',
'lead_name': 'Test Lead',
'email_id': 'test@example.com'
})
test_lead.insert(ignore_permissions=True)
return test_lead
def create_test_appointments():
test_appointment = frappe.db.exists(
{'doctype': 'Appointment', 'scheduled_time':datetime.datetime.now(),'email':'test@example.com'})
if test_appointment:
return frappe.get_doc('Appointment', test_appointment[0][0])
test_appointment = frappe.get_doc({
'doctype': 'Appointment',
'email': 'test@example.com',
'status': 'Open',
'customer_name': 'Test Lead',
'customer_phone_number': '666',
'customer_skype': 'test',
'customer_email': 'test@example.com',
'scheduled_time': datetime.datetime.now()
})
test_appointment.insert()
return test_appointment
class TestAppointment(unittest.TestCase):
test_appointment = test_lead = None
def setUp(self):
self.test_lead = create_test_lead()
self.test_appointment = create_test_appointments()
def test_calendar_event_created(self):
cal_event = frappe.get_doc(
'Event', self.test_appointment.calendar_event)
self.assertEqual(cal_event.starts_on,
self.test_appointment.scheduled_time)
def test_lead_linked(self):
lead = frappe.get_doc('Lead', self.test_lead.name)
self.assertIsNotNone(lead)

View File

@@ -0,0 +1,10 @@
frappe.ui.form.on('Appointment Booking Settings', 'validate',check_times);
function check_times(frm) {
$.each(frm.doc.availability_of_slots || [], function (i, d) {
let from_time = Date.parse('01/01/2019 ' + d.from_time);
let to_time = Date.parse('01/01/2019 ' + d.to_time);
if (from_time > to_time) {
frappe.throw(__(`In row ${i + 1} of Appointment Booking Slots : "To Time" must be later than "From Time"`));
}
});
}

View File

@@ -0,0 +1,151 @@
{
"creation": "2019-08-27 10:56:48.309824",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"enable_scheduling",
"agent_detail_section",
"availability_of_slots",
"number_of_agents",
"agent_list",
"holiday_list",
"appointment_details_section",
"appointment_duration",
"email_reminders",
"advance_booking_days",
"success_details",
"success_redirect_url"
],
"fields": [
{
"fieldname": "availability_of_slots",
"fieldtype": "Table",
"label": "Availability Of Slots",
"options": "Appointment Booking Slots",
"reqd": 1
},
{
"default": "1",
"fieldname": "number_of_agents",
"fieldtype": "Int",
"hidden": 1,
"in_list_view": 1,
"label": "Number of Concurrent Appointments",
"read_only": 1,
"reqd": 1
},
{
"fieldname": "holiday_list",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Holiday List",
"options": "Holiday List",
"reqd": 1
},
{
"default": "60",
"fieldname": "appointment_duration",
"fieldtype": "Int",
"label": "Appointment Duration (In Minutes)",
"reqd": 1
},
{
"default": "0",
"description": "Notify customer and agent via email on the day of the appointment.",
"fieldname": "email_reminders",
"fieldtype": "Check",
"label": "Notify Via Email"
},
{
"default": "7",
"fieldname": "advance_booking_days",
"fieldtype": "Int",
"label": "Number of days appointments can be booked in advance",
"reqd": 1
},
{
"fieldname": "agent_list",
"fieldtype": "Table MultiSelect",
"label": "Agents",
"options": "Assignment Rule User",
"reqd": 1
},
{
"default": "0",
"fieldname": "enable_scheduling",
"fieldtype": "Check",
"label": "Enable Appointment Scheduling",
"reqd": 1
},
{
"fieldname": "agent_detail_section",
"fieldtype": "Section Break",
"label": "Agent Details"
},
{
"fieldname": "appointment_details_section",
"fieldtype": "Section Break",
"label": "Appointment Details"
},
{
"fieldname": "success_details",
"fieldtype": "Section Break",
"label": "Success Settings"
},
{
"description": "Leave blank for home.\nThis is relative to site URL, for example \"about\" will redirect to \"https://yoursitename.com/about\"",
"fieldname": "success_redirect_url",
"fieldtype": "Data",
"label": "Success Redirect URL"
}
],
"issingle": 1,
"modified": "2019-11-26 12:14:17.669366",
"modified_by": "Administrator",
"module": "CRM",
"name": "Appointment Booking Settings",
"owner": "Administrator",
"permissions": [
{
"create": 1,
"delete": 1,
"email": 1,
"print": 1,
"read": 1,
"role": "System Manager",
"share": 1,
"write": 1
},
{
"email": 1,
"print": 1,
"read": 1,
"role": "Guest",
"share": 1
},
{
"create": 1,
"email": 1,
"print": 1,
"read": 1,
"role": "HR Manager",
"share": 1,
"write": 1
},
{
"create": 1,
"delete": 1,
"email": 1,
"print": 1,
"read": 1,
"role": "Sales Manager",
"share": 1,
"write": 1
}
],
"quick_entry": 1,
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
}

View File

@@ -0,0 +1,43 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
from __future__ import unicode_literals
import frappe
from frappe import _
import datetime
from frappe.model.document import Document
class AppointmentBookingSettings(Document):
agent_list = [] #Hack
min_date = '01/01/1970 '
format_string = "%d/%m/%Y %H:%M:%S"
def validate(self):
self.validate_availability_of_slots()
def save(self):
self.number_of_agents = len(self.agent_list)
super(AppointmentBookingSettings, self).save()
def validate_availability_of_slots(self):
for record in self.availability_of_slots:
from_time = datetime.datetime.strptime(
self.min_date+record.from_time, self.format_string)
to_time = datetime.datetime.strptime(
self.min_date+record.to_time, self.format_string)
timedelta = to_time-from_time
self.validate_from_and_to_time(from_time, to_time)
self.duration_is_divisible(from_time, to_time)
def validate_from_and_to_time(self, from_time, to_time):
if from_time > to_time:
err_msg = _('<b>From Time</b> cannot be later than <b>To Time</b> for {0}').format(record.day_of_week)
frappe.throw(_(err_msg))
def duration_is_divisible(self, from_time, to_time):
timedelta = to_time - from_time
if timedelta.total_seconds() % (self.appointment_duration * 60):
frappe.throw(
_('The difference between from time and To Time must be a multiple of Appointment'))

View File

@@ -0,0 +1,10 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
from __future__ import unicode_literals
# import frappe
import unittest
class TestAppointmentBookingSettings(unittest.TestCase):
pass

View File

@@ -0,0 +1,46 @@
{
"creation": "2019-11-19 10:49:49.494927",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"day_of_week",
"from_time",
"to_time"
],
"fields": [
{
"fieldname": "day_of_week",
"fieldtype": "Select",
"in_list_view": 1,
"label": "Day Of Week",
"options": "Sunday\nMonday\nTuesday\nWednesday\nThursday\nFriday\nSaturday",
"reqd": 1
},
{
"fieldname": "from_time",
"fieldtype": "Time",
"in_list_view": 1,
"label": "From Time ",
"reqd": 1
},
{
"fieldname": "to_time",
"fieldtype": "Time",
"in_list_view": 1,
"label": "To Time",
"reqd": 1
}
],
"istable": 1,
"modified": "2019-11-19 10:49:49.494927",
"modified_by": "Administrator",
"module": "CRM",
"name": "Appointment Booking Slots",
"owner": "Administrator",
"permissions": [],
"quick_entry": 1,
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
}

View File

@@ -0,0 +1,10 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
from __future__ import unicode_literals
# import frappe
from frappe.model.document import Document
class AppointmentBookingSlots(Document):
pass

View File

@@ -0,0 +1,46 @@
{
"creation": "2019-09-10 15:02:05.779434",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"day_of_week",
"from_time",
"to_time"
],
"fields": [
{
"fieldname": "day_of_week",
"fieldtype": "Select",
"in_list_view": 1,
"label": "Day Of Week",
"options": "Sunday\nMonday\nTuesday\nWednesday\nThursday\nFriday\nSaturday",
"reqd": 1
},
{
"fieldname": "from_time",
"fieldtype": "Time",
"in_list_view": 1,
"label": "From Time",
"reqd": 1
},
{
"fieldname": "to_time",
"fieldtype": "Time",
"in_list_view": 1,
"label": "To Time",
"reqd": 1
}
],
"istable": 1,
"modified": "2019-09-10 15:05:20.406855",
"modified_by": "Administrator",
"module": "CRM",
"name": "Availability Of Slots",
"owner": "Administrator",
"permissions": [],
"quick_entry": 1,
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
}

View File

@@ -0,0 +1,10 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
from __future__ import unicode_literals
# import frappe
from frappe.model.document import Document
class AvailabilityOfSlots(Document):
pass

View File

@@ -41,7 +41,8 @@ class EmailCampaign(Document):
email_campaign_exists = frappe.db.exists("Email Campaign", { email_campaign_exists = frappe.db.exists("Email Campaign", {
"campaign_name": self.campaign_name, "campaign_name": self.campaign_name,
"recipient": self.recipient, "recipient": self.recipient,
"status": ("in", ["In Progress", "Scheduled"]) "status": ("in", ["In Progress", "Scheduled"]),
"name": ("!=", self.name)
}) })
if email_campaign_exists: if email_campaign_exists:
frappe.throw(_("The Campaign '{0}' already exists for the {1} '{2}'").format(self.campaign_name, self.email_campaign_for, self.recipient)) frappe.throw(_("The Campaign '{0}' already exists for the {1} '{2}'").format(self.campaign_name, self.email_campaign_for, self.recipient))
@@ -78,7 +79,7 @@ def send_mail(entry, email_campaign):
comm = make( comm = make(
doctype = "Email Campaign", doctype = "Email Campaign",
name = email_campaign.name, name = email_campaign.name,
subject = email_template.get("subject"), subject = frappe.render_template(email_template.get("subject"), context),
content = frappe.render_template(email_template.get("response"), context), content = frappe.render_template(email_template.get("response"), context),
sender = sender, sender = sender,
recipients = recipient, recipients = recipient,

File diff suppressed because it is too large Load Diff

View File

@@ -130,10 +130,11 @@ class Opportunity(TransactionBase):
def has_lost_quotation(self): def has_lost_quotation(self):
lost_quotation = frappe.db.sql(""" lost_quotation = frappe.db.sql("""
select q.name select name
from `tabQuotation` q, `tabQuotation Item` qi from `tabQuotation`
where q.name = qi.parent and q.docstatus=1 where docstatus=1
and qi.prevdoc_docname =%s and q.status = 'Lost' and opportunity =%s
and status = 'Lost'
""", self.name) """, self.name)
if lost_quotation: if lost_quotation:
if self.has_active_quotation(): if self.has_active_quotation():

View File

@@ -4,11 +4,11 @@ cur_frm.add_fetch("employee", "image", "image");
frappe.ui.form.on("Instructor", { frappe.ui.form.on("Instructor", {
employee: function(frm) { employee: function(frm) {
if(!frm.doc.employee) return; if(!frm.doc.employee) return;
frappe.db.get_value('Employee', {name: frm.doc.employee}, 'company', (company) => { frappe.db.get_value('Employee', {name: frm.doc.employee}, 'company', (d) => {
frm.set_query("department", function() { frm.set_query("department", function() {
return { return {
"filters": { "filters": {
"company": company, "company": d.company,
} }
}; };
}); });
@@ -16,7 +16,7 @@ frappe.ui.form.on("Instructor", {
frm.set_query("department", "instructor_log", function() { frm.set_query("department", "instructor_log", function() {
return { return {
"filters": { "filters": {
"company": company, "company": d.company,
} }
}; };
}); });

View File

@@ -40,7 +40,7 @@ class Student(Document):
frappe.throw(_("Student {0} exist against student applicant {1}").format(student[0][0], self.student_applicant)) frappe.throw(_("Student {0} exist against student applicant {1}").format(student[0][0], self.student_applicant))
def after_insert(self): def after_insert(self):
if not frappe.get_single('Education Settings').user_creation_skip: if not frappe.get_single('Education Settings').get('user_creation_skip'):
self.create_student_user() self.create_student_user()
def create_student_user(self): def create_student_user(self):

View File

@@ -182,6 +182,7 @@ standard_portal_menu_items = [
{"title": _("Admission"), "route": "/admissions", "reference_doctype": "Student Admission", "role": "Student"}, {"title": _("Admission"), "route": "/admissions", "reference_doctype": "Student Admission", "role": "Student"},
{"title": _("Certification"), "route": "/certification", "reference_doctype": "Certification Application", "role": "Non Profit Portal User"}, {"title": _("Certification"), "route": "/certification", "reference_doctype": "Certification Application", "role": "Non Profit Portal User"},
{"title": _("Material Request"), "route": "/material-requests", "reference_doctype": "Material Request", "role": "Customer"}, {"title": _("Material Request"), "route": "/material-requests", "reference_doctype": "Material Request", "role": "Customer"},
{"title": _("Appointment Booking"), "route": "/book_appointment"},
] ]
default_roles = [ default_roles = [
@@ -248,10 +249,10 @@ doc_events = {
"on_trash": "erpnext.regional.check_deletion_permission" "on_trash": "erpnext.regional.check_deletion_permission"
}, },
'Address': { 'Address': {
'validate': ['erpnext.regional.india.utils.validate_gstin_for_india', 'erpnext.regional.italy.utils.set_state_code'] 'validate': ['erpnext.regional.india.utils.validate_gstin_for_india', 'erpnext.regional.italy.utils.set_state_code', 'erpnext.regional.india.utils.update_gst_category']
}, },
('Sales Invoice', 'Purchase Invoice', 'Delivery Note'): { ('Sales Invoice', 'Sales Order', 'Delivery Note', 'Purchase Invoice', 'Purchase Order', 'Purchase Receipt'): {
'validate': 'erpnext.regional.india.utils.set_place_of_supply' 'validate': ['erpnext.regional.india.utils.set_place_of_supply']
}, },
"Contact": { "Contact": {
"on_trash": "erpnext.support.doctype.issue.issue.update_issue", "on_trash": "erpnext.support.doctype.issue.issue.update_issue",
@@ -301,7 +302,8 @@ scheduler_events = {
"erpnext.quality_management.doctype.quality_review.quality_review.review", "erpnext.quality_management.doctype.quality_review.quality_review.review",
"erpnext.support.doctype.service_level_agreement.service_level_agreement.check_agreement_status", "erpnext.support.doctype.service_level_agreement.service_level_agreement.check_agreement_status",
"erpnext.crm.doctype.email_campaign.email_campaign.send_email_to_leads_or_contacts", "erpnext.crm.doctype.email_campaign.email_campaign.send_email_to_leads_or_contacts",
"erpnext.crm.doctype.email_campaign.email_campaign.set_email_campaign_status" "erpnext.crm.doctype.email_campaign.email_campaign.set_email_campaign_status",
"erpnext.selling.doctype.quotation.quotation.set_expired_status"
], ],
"daily_long": [ "daily_long": [
"erpnext.setup.doctype.email_digest.email_digest.send", "erpnext.setup.doctype.email_digest.email_digest.send",

View File

@@ -19,4 +19,4 @@ frappe.ui.form.on('Compensatory Leave Request', {
frm.set_df_property('half_day_date', 'reqd', false); frm.set_df_property('half_day_date', 'reqd', false);
} }
} }
}); });

View File

@@ -5,9 +5,10 @@
from __future__ import unicode_literals from __future__ import unicode_literals
import frappe import frappe
from frappe import _ from frappe import _
from frappe.utils import date_diff, add_days, getdate from frappe.utils import date_diff, add_days, getdate, cint
from frappe.model.document import Document from frappe.model.document import Document
from erpnext.hr.utils import validate_dates, validate_overlap, get_leave_period, get_holidays_for_employee from erpnext.hr.utils import validate_dates, validate_overlap, get_leave_period, \
get_holidays_for_employee, create_additional_leave_ledger_entry
class CompensatoryLeaveRequest(Document): class CompensatoryLeaveRequest(Document):
@@ -25,16 +26,14 @@ class CompensatoryLeaveRequest(Document):
frappe.throw(_("Leave Type is madatory")) frappe.throw(_("Leave Type is madatory"))
def validate_attendance(self): def validate_attendance(self):
query = """select attendance_date, status attendance = frappe.get_all('Attendance',
from `tabAttendance` where filters={
attendance_date between %(work_from_date)s and %(work_end_date)s 'attendance_date': ['between', (self.work_from_date, self.work_end_date)],
and docstatus=1 and status = 'Present' and employee=%(employee)s""" 'status': 'Present',
'docstatus': 1,
'employee': self.employee
}, fields=['attendance_date', 'status'])
attendance = frappe.db.sql(query, {
"work_from_date": self.work_from_date,
"work_end_date": self.work_end_date,
"employee": self.employee
}, as_dict=True)
if len(attendance) < date_diff(self.work_end_date, self.work_from_date) + 1: if len(attendance) < date_diff(self.work_end_date, self.work_from_date) + 1:
frappe.throw(_("You are not present all day(s) between compensatory leave request days")) frappe.throw(_("You are not present all day(s) between compensatory leave request days"))
@@ -50,13 +49,19 @@ class CompensatoryLeaveRequest(Document):
date_difference -= 0.5 date_difference -= 0.5
leave_period = get_leave_period(self.work_from_date, self.work_end_date, company) leave_period = get_leave_period(self.work_from_date, self.work_end_date, company)
if leave_period: if leave_period:
leave_allocation = self.exists_allocation_for_period(leave_period) leave_allocation = self.get_existing_allocation_for_period(leave_period)
if leave_allocation: if leave_allocation:
leave_allocation.new_leaves_allocated += date_difference leave_allocation.new_leaves_allocated += date_difference
leave_allocation.submit() leave_allocation.validate()
leave_allocation.db_set("new_leaves_allocated", leave_allocation.total_leaves_allocated)
leave_allocation.db_set("total_leaves_allocated", leave_allocation.total_leaves_allocated)
# generate additional ledger entry for the new compensatory leaves off
create_additional_leave_ledger_entry(leave_allocation, date_difference, add_days(self.work_end_date, 1))
else: else:
leave_allocation = self.create_leave_allocation(leave_period, date_difference) leave_allocation = self.create_leave_allocation(leave_period, date_difference)
self.db_set("leave_allocation", leave_allocation.name) self.leave_allocation=leave_allocation.name
else: else:
frappe.throw(_("There is no leave period in between {0} and {1}").format(self.work_from_date, self.work_end_date)) frappe.throw(_("There is no leave period in between {0} and {1}").format(self.work_from_date, self.work_end_date))
@@ -68,11 +73,16 @@ class CompensatoryLeaveRequest(Document):
leave_allocation = frappe.get_doc("Leave Allocation", self.leave_allocation) leave_allocation = frappe.get_doc("Leave Allocation", self.leave_allocation)
if leave_allocation: if leave_allocation:
leave_allocation.new_leaves_allocated -= date_difference leave_allocation.new_leaves_allocated -= date_difference
if leave_allocation.total_leaves_allocated - date_difference <= 0: if leave_allocation.new_leaves_allocated - date_difference <= 0:
leave_allocation.total_leaves_allocated = 0 leave_allocation.new_leaves_allocated = 0
leave_allocation.submit() leave_allocation.validate()
leave_allocation.db_set("new_leaves_allocated", leave_allocation.total_leaves_allocated)
leave_allocation.db_set("total_leaves_allocated", leave_allocation.total_leaves_allocated)
def exists_allocation_for_period(self, leave_period): # create reverse entry on cancelation
create_additional_leave_ledger_entry(leave_allocation, date_difference * -1, add_days(self.work_end_date, 1))
def get_existing_allocation_for_period(self, leave_period):
leave_allocation = frappe.db.sql(""" leave_allocation = frappe.db.sql("""
select name select name
from `tabLeave Allocation` from `tabLeave Allocation`
@@ -95,17 +105,18 @@ class CompensatoryLeaveRequest(Document):
def create_leave_allocation(self, leave_period, date_difference): def create_leave_allocation(self, leave_period, date_difference):
is_carry_forward = frappe.db.get_value("Leave Type", self.leave_type, "is_carry_forward") is_carry_forward = frappe.db.get_value("Leave Type", self.leave_type, "is_carry_forward")
allocation = frappe.new_doc("Leave Allocation") allocation = frappe.get_doc(dict(
allocation.employee = self.employee doctype="Leave Allocation",
allocation.employee_name = self.employee_name employee=self.employee,
allocation.leave_type = self.leave_type employee_name=self.employee_name,
allocation.from_date = add_days(self.work_end_date, 1) leave_type=self.leave_type,
allocation.to_date = leave_period[0].to_date from_date=add_days(self.work_end_date, 1),
allocation.new_leaves_allocated = date_difference to_date=leave_period[0].to_date,
allocation.total_leaves_allocated = date_difference carry_forward=cint(is_carry_forward),
allocation.description = self.reason new_leaves_allocated=date_difference,
if is_carry_forward == 1: total_leaves_allocated=date_difference,
allocation.carry_forward = True description=self.reason
allocation.save(ignore_permissions = True) ))
allocation.insert(ignore_permissions=True)
allocation.submit() allocation.submit()
return allocation return allocation

View File

@@ -5,37 +5,128 @@ from __future__ import unicode_literals
import frappe import frappe
import unittest import unittest
from frappe.utils import today, add_months, add_days
from erpnext.hr.doctype.attendance_request.test_attendance_request import get_employee
from erpnext.hr.doctype.leave_period.test_leave_period import create_leave_period
from erpnext.hr.doctype.leave_application.leave_application import get_leave_balance_on
# class TestCompensatoryLeaveRequest(unittest.TestCase): class TestCompensatoryLeaveRequest(unittest.TestCase):
# def get_compensatory_leave_request(self): def setUp(self):
# return frappe.get_doc('Compensatory Leave Request', dict( frappe.db.sql(''' delete from `tabCompensatory Leave Request`''')
# employee = employee, frappe.db.sql(''' delete from `tabLeave Ledger Entry`''')
# work_from_date = today, frappe.db.sql(''' delete from `tabLeave Allocation`''')
# work_to_date = today, frappe.db.sql(''' delete from `tabAttendance` where attendance_date in {0} '''.format((today(), add_days(today(), -1)))) #nosec
# reason = 'test' create_leave_period(add_months(today(), -3), add_months(today(), 3), "_Test Company")
# )).insert() create_holiday_list()
#
# def test_creation_of_leave_allocation(self): employee = get_employee()
# employee = get_employee() employee.holiday_list = "_Test Compensatory Leave"
# today = get_today() employee.save()
#
# compensatory_leave_request = self.get_compensatory_leave_request(today) def test_leave_balance_on_submit(self):
# ''' check creation of leave allocation on submission of compensatory leave request '''
# before = get_leave_balance(employee, compensatory_leave_request.leave_type) employee = get_employee()
# mark_attendance(employee)
# compensatory_leave_request.submit() compensatory_leave_request = get_compensatory_leave_request(employee.name)
#
# self.assertEqual(get_leave_balance(employee, compensatory_leave_request.leave_type), before + 1) before = get_leave_balance_on(employee.name, compensatory_leave_request.leave_type, today())
# compensatory_leave_request.submit()
# def test_max_compensatory_leave(self):
# employee = get_employee() self.assertEqual(get_leave_balance_on(employee.name, compensatory_leave_request.leave_type, add_days(today(), 1)), before + 1)
# today = get_today()
# def test_leave_allocation_update_on_submit(self):
# compensatory_leave_request = self.get_compensatory_leave_request() employee = get_employee()
# mark_attendance(employee, date=add_days(today(), -1))
# frappe.db.set_value('Leave Type', compensatory_leave_request.leave_type, 'max_leaves_allowed', 0) compensatory_leave_request = get_compensatory_leave_request(employee.name, leave_date=add_days(today(), -1))
# compensatory_leave_request.submit()
# self.assertRaises(MaxLeavesLimitCrossed, compensatory_leave_request.submit)
# # leave allocation creation on submit
# frappe.db.set_value('Leave Type', compensatory_leave_request.leave_type, 'max_leaves_allowed', 10) leaves_allocated = frappe.db.get_value('Leave Allocation', {
# 'name': compensatory_leave_request.leave_allocation
}, ['total_leaves_allocated'])
self.assertEqual(leaves_allocated, 1)
mark_attendance(employee)
compensatory_leave_request = get_compensatory_leave_request(employee.name)
compensatory_leave_request.submit()
# leave allocation updates on submission of second compensatory leave request
leaves_allocated = frappe.db.get_value('Leave Allocation', {
'name': compensatory_leave_request.leave_allocation
}, ['total_leaves_allocated'])
self.assertEqual(leaves_allocated, 2)
def test_creation_of_leave_ledger_entry_on_submit(self):
''' check creation of leave ledger entry on submission of leave request '''
employee = get_employee()
mark_attendance(employee)
compensatory_leave_request = get_compensatory_leave_request(employee.name)
compensatory_leave_request.submit()
filters = dict(transaction_name=compensatory_leave_request.leave_allocation)
leave_ledger_entry = frappe.get_all('Leave Ledger Entry', fields='*', filters=filters)
self.assertEquals(len(leave_ledger_entry), 1)
self.assertEquals(leave_ledger_entry[0].employee, compensatory_leave_request.employee)
self.assertEquals(leave_ledger_entry[0].leave_type, compensatory_leave_request.leave_type)
self.assertEquals(leave_ledger_entry[0].leaves, 1)
# check reverse leave ledger entry on cancellation
compensatory_leave_request.cancel()
leave_ledger_entry = frappe.get_all('Leave Ledger Entry', fields='*', filters=filters, order_by = 'creation desc')
self.assertEquals(len(leave_ledger_entry), 2)
self.assertEquals(leave_ledger_entry[0].employee, compensatory_leave_request.employee)
self.assertEquals(leave_ledger_entry[0].leave_type, compensatory_leave_request.leave_type)
self.assertEquals(leave_ledger_entry[0].leaves, -1)
def get_compensatory_leave_request(employee, leave_date=today()):
prev_comp_leave_req = frappe.db.get_value('Compensatory Leave Request',
dict(leave_type='Compensatory Off',
work_from_date=leave_date,
work_end_date=leave_date,
employee=employee), 'name')
if prev_comp_leave_req:
return frappe.get_doc('Compensatory Leave Request', prev_comp_leave_req)
return frappe.get_doc(dict(
doctype='Compensatory Leave Request',
employee=employee,
leave_type='Compensatory Off',
work_from_date=leave_date,
work_end_date=leave_date,
reason='test'
)).insert()
def mark_attendance(employee, date=today(), status='Present'):
if not frappe.db.exists(dict(doctype='Attendance', employee=employee.name, attendance_date=date, status='Present')):
attendance = frappe.get_doc({
"doctype": "Attendance",
"employee": employee.name,
"attendance_date": date,
"status": status
})
attendance.save()
attendance.submit()
def create_holiday_list():
if frappe.db.exists("Holiday List", "_Test Compensatory Leave"):
return
holiday_list = frappe.get_doc({
"doctype": "Holiday List",
"from_date": add_months(today(), -3),
"to_date": add_months(today(), 3),
"holidays": [
{
"description": "Test Holiday",
"holiday_date": today()
},
{
"description": "Test Holiday 1",
"holiday_date": add_days(today(), -1)
}
],
"holiday_list_name": "_Test Compensatory Leave"
})
holiday_list.save()

View File

@@ -4,7 +4,7 @@
from __future__ import unicode_literals from __future__ import unicode_literals
import frappe import frappe
from frappe.utils import getdate, validate_email_address, today, add_years, format_datetime from frappe.utils import getdate, validate_email_address, today, add_years, format_datetime, cstr
from frappe.model.naming import set_name_by_naming_series from frappe.model.naming import set_name_by_naming_series
from frappe import throw, _, scrub from frappe import throw, _, scrub
from frappe.permissions import add_user_permission, remove_user_permission, \ from frappe.permissions import add_user_permission, remove_user_permission, \
@@ -218,8 +218,8 @@ class Employee(NestedSet):
def reset_employee_emails_cache(self): def reset_employee_emails_cache(self):
prev_doc = self.get_doc_before_save() or {} prev_doc = self.get_doc_before_save() or {}
cell_number = self.get('cell_number') cell_number = cstr(self.get('cell_number'))
prev_number = prev_doc.get('cell_number') prev_number = cstr(prev_doc.get('cell_number'))
if (cell_number != prev_number or if (cell_number != prev_number or
self.get('user_id') != prev_doc.get('user_id')): self.get('user_id') != prev_doc.get('user_id')):
frappe.cache().hdel('employees_with_number', cell_number) frappe.cache().hdel('employees_with_number', cell_number)

View File

@@ -6,6 +6,7 @@ from __future__ import unicode_literals
import frappe import frappe
import json import json
from frappe.model.document import Document from frappe.model.document import Document
from frappe.utils import getdate
class EmployeeAttendanceTool(Document): class EmployeeAttendanceTool(Document):
@@ -43,17 +44,26 @@ def get_employees(date, department = None, branch = None, company = None):
@frappe.whitelist() @frappe.whitelist()
def mark_employee_attendance(employee_list, status, date, leave_type=None, company=None): def mark_employee_attendance(employee_list, status, date, leave_type=None, company=None):
employee_list = json.loads(employee_list) employee_list = json.loads(employee_list)
for employee in employee_list: for employee in employee_list:
attendance = frappe.new_doc("Attendance")
attendance.employee = employee['employee']
attendance.employee_name = employee['employee_name']
attendance.attendance_date = date
attendance.status = status
if status == "On Leave" and leave_type: if status == "On Leave" and leave_type:
attendance.leave_type = leave_type leave_type = leave_type
if company:
attendance.company = company
else: else:
attendance.company = frappe.db.get_value("Employee", employee['employee'], "Company") leave_type = None
if not company:
company = frappe.db.get_value("Employee", employee['employee'], "Company")
attendance=frappe.get_doc(dict(
doctype='Attendance',
employee=employee.get('employee'),
employee_name=employee.get('employee_name'),
attendance_date=getdate(date),
status=status,
leave_type=leave_type,
company=company
))
attendance.insert()
attendance.submit() attendance.submit()

View File

@@ -208,6 +208,24 @@ frappe.ui.form.on("Expense Claim", {
frm.refresh_fields(); frm.refresh_fields();
}, },
grand_total: function(frm) {
frm.trigger("update_employee_advance_claimed_amount");
},
update_employee_advance_claimed_amount: function(frm) {
let amount_to_be_allocated = frm.doc.grand_total;
$.each(frm.doc.advances || [], function(i, advance){
if (amount_to_be_allocated >= advance.unclaimed_amount){
frm.doc.advances[i].allocated_amount = frm.doc.advances[i].unclaimed_amount;
amount_to_be_allocated -= advance.allocated_amount;
} else{
frm.doc.advances[i].allocated_amount = amount_to_be_allocated;
amount_to_be_allocated = 0;
}
frm.refresh_field("advances");
});
},
make_payment_entry: function(frm) { make_payment_entry: function(frm) {
var method = "erpnext.accounts.doctype.payment_entry.payment_entry.get_payment_entry"; var method = "erpnext.accounts.doctype.payment_entry.payment_entry.get_payment_entry";
if(frm.doc.__onload && frm.doc.__onload.make_payment_via_journal_entry) { if(frm.doc.__onload && frm.doc.__onload.make_payment_via_journal_entry) {
@@ -300,7 +318,7 @@ frappe.ui.form.on("Expense Claim", {
row.advance_account = d.advance_account; row.advance_account = d.advance_account;
row.advance_paid = d.paid_amount; row.advance_paid = d.paid_amount;
row.unclaimed_amount = flt(d.paid_amount) - flt(d.claimed_amount); row.unclaimed_amount = flt(d.paid_amount) - flt(d.claimed_amount);
row.allocated_amount = flt(d.paid_amount) - flt(d.claimed_amount); row.allocated_amount = 0;
}); });
refresh_field("advances"); refresh_field("advances");
} }

View File

@@ -43,9 +43,9 @@ class ExpenseClaim(AccountsController):
}[cstr(self.docstatus or 0)] }[cstr(self.docstatus or 0)]
paid_amount = flt(self.total_amount_reimbursed) + flt(self.total_advance_amount) paid_amount = flt(self.total_amount_reimbursed) + flt(self.total_advance_amount)
precision = self.precision("total_sanctioned_amount") precision = self.precision("grand_total")
if (self.is_paid or (flt(self.total_sanctioned_amount) > 0 if (self.is_paid or (flt(self.total_sanctioned_amount) > 0
and flt(self.total_sanctioned_amount, precision) == flt(paid_amount, precision))) \ and flt(self.grand_total, precision) == flt(paid_amount, precision))) \
and self.docstatus == 1 and self.approval_status == 'Approved': and self.docstatus == 1 and self.approval_status == 'Approved':
self.status = "Paid" self.status = "Paid"
elif flt(self.total_sanctioned_amount) > 0 and self.docstatus == 1 and self.approval_status == 'Approved': elif flt(self.total_sanctioned_amount) > 0 and self.docstatus == 1 and self.approval_status == 'Approved':

View File

@@ -69,10 +69,14 @@ class LeaveAllocation(Document):
def validate_allocation_overlap(self): def validate_allocation_overlap(self):
leave_allocation = frappe.db.sql(""" leave_allocation = frappe.db.sql("""
select name from `tabLeave Allocation` SELECT
where employee=%s and leave_type=%s and docstatus=1 name
and to_date >= %s and from_date <= %s""", FROM `tabLeave Allocation`
(self.employee, self.leave_type, self.from_date, self.to_date)) WHERE
employee=%s AND leave_type=%s
AND name <> %s AND docstatus=1
AND to_date >= %s AND from_date <= %s""",
(self.employee, self.leave_type, self.name, self.from_date, self.to_date))
if leave_allocation: if leave_allocation:
frappe.msgprint(_("{0} already allocated for Employee {1} for period {2} to {3}") frappe.msgprint(_("{0} already allocated for Employee {1} for period {2} to {3}")

View File

@@ -549,10 +549,10 @@ def get_leaves_for_period(employee, leave_type, from_date, to_date):
leave_days += leave_entry.leaves leave_days += leave_entry.leaves
elif inclusive_period and leave_entry.transaction_type == 'Leave Allocation' \ elif inclusive_period and leave_entry.transaction_type == 'Leave Allocation' \
and not skip_expiry_leaves(leave_entry, to_date): and leave_entry.is_expired and not skip_expiry_leaves(leave_entry, to_date):
leave_days += leave_entry.leaves leave_days += leave_entry.leaves
else: elif leave_entry.transaction_type == 'Leave Application':
if leave_entry.from_date < getdate(from_date): if leave_entry.from_date < getdate(from_date):
leave_entry.from_date = from_date leave_entry.from_date = from_date
if leave_entry.to_date > getdate(to_date): if leave_entry.to_date > getdate(to_date):
@@ -579,14 +579,15 @@ def skip_expiry_leaves(leave_entry, date):
def get_leave_entries(employee, leave_type, from_date, to_date): def get_leave_entries(employee, leave_type, from_date, to_date):
''' Returns leave entries between from_date and to_date ''' ''' Returns leave entries between from_date and to_date '''
return frappe.db.sql(""" return frappe.db.sql("""
select employee, leave_type, from_date, to_date, leaves, transaction_type, is_carry_forward, transaction_name SELECT
from `tabLeave Ledger Entry` employee, leave_type, from_date, to_date, leaves, transaction_name, transaction_type,
where employee=%(employee)s and leave_type=%(leave_type)s is_carry_forward, is_expired
and docstatus=1 FROM `tabLeave Ledger Entry`
and leaves<0 WHERE employee=%(employee)s AND leave_type=%(leave_type)s
and (from_date between %(from_date)s and %(to_date)s AND docstatus=1 AND leaves<0
or to_date between %(from_date)s and %(to_date)s AND (from_date between %(from_date)s AND %(to_date)s
or (from_date < %(from_date)s and to_date > %(to_date)s)) OR to_date between %(from_date)s AND %(to_date)s
OR (from_date < %(from_date)s AND to_date > %(to_date)s))
""", { """, {
"from_date": from_date, "from_date": from_date,
"to_date": to_date, "to_date": to_date,
@@ -773,4 +774,4 @@ def get_leave_approver(employee):
leave_approver = frappe.db.get_value('Department Approver', {'parent': department, leave_approver = frappe.db.get_value('Department Approver', {'parent': department,
'parentfield': 'leave_approvers', 'idx': 1}, 'approver') 'parentfield': 'leave_approvers', 'idx': 1}, 'approver')
return leave_approver return leave_approver

View File

@@ -43,10 +43,18 @@ class TestLeavePeriod(unittest.TestCase):
leave_period.grant_leave_allocation(employee=employee_doc_name) leave_period.grant_leave_allocation(employee=employee_doc_name)
self.assertEqual(get_leave_balance_on(employee_doc_name, leave_type, today()), 20) self.assertEqual(get_leave_balance_on(employee_doc_name, leave_type, today()), 20)
def create_leave_period(from_date, to_date): def create_leave_period(from_date, to_date, company=None):
leave_period = frappe.db.get_value('Leave Period',
dict(company=company or erpnext.get_default_company(),
from_date=from_date,
to_date=to_date,
is_active=1), 'name')
if leave_period:
return frappe.get_doc("Leave Period", leave_period)
leave_period = frappe.get_doc({ leave_period = frappe.get_doc({
"doctype": "Leave Period", "doctype": "Leave Period",
"company": erpnext.get_default_company(), "company": company or erpnext.get_default_company(),
"from_date": from_date, "from_date": from_date,
"to_date": to_date, "to_date": to_date,
"is_active": 1 "is_active": 1

View File

@@ -321,11 +321,11 @@ def allocate_earned_leaves():
if new_allocation == allocation.total_leaves_allocated: if new_allocation == allocation.total_leaves_allocated:
continue continue
allocation.db_set("total_leaves_allocated", new_allocation, update_modified=False) allocation.db_set("total_leaves_allocated", new_allocation, update_modified=False)
create_earned_leave_ledger_entry(allocation, earned_leaves, today) create_additional_leave_ledger_entry(allocation, earned_leaves, today)
def create_earned_leave_ledger_entry(allocation, earned_leaves, date): def create_additional_leave_ledger_entry(allocation, leaves, date):
''' Create leave ledger entry based on the earned leave frequency ''' ''' Create leave ledger entry for leave types '''
allocation.new_leaves_allocated = earned_leaves allocation.new_leaves_allocated = leaves
allocation.from_date = date allocation.from_date = date
allocation.unused_leaves = 0 allocation.unused_leaves = 0
allocation.create_leave_ledger_entry() allocation.create_leave_ledger_entry()
@@ -389,6 +389,7 @@ def get_sal_slip_total_benefit_given(employee, payroll_period, component=False):
def get_holidays_for_employee(employee, start_date, end_date): def get_holidays_for_employee(employee, start_date, end_date):
holiday_list = get_holiday_list_for_employee(employee) holiday_list = get_holiday_list_for_employee(employee)
holidays = frappe.db.sql_list('''select holiday_date from `tabHoliday` holidays = frappe.db.sql_list('''select holiday_date from `tabHoliday`
where where
parent=%(holiday_list)s parent=%(holiday_list)s
@@ -437,4 +438,4 @@ def get_previous_claimed_amount(employee, payroll_period, non_pro_rata=False, co
}, as_dict=True) }, as_dict=True)
if sum_of_claimed_amount and flt(sum_of_claimed_amount[0].total_amount) > 0: if sum_of_claimed_amount and flt(sum_of_claimed_amount[0].total_amount) > 0:
total_claimed_amount = sum_of_claimed_amount[0].total_amount total_claimed_amount = sum_of_claimed_amount[0].total_amount
return total_claimed_amount return total_claimed_amount

View File

@@ -89,7 +89,8 @@
"fieldtype": "Link", "fieldtype": "Link",
"label": "Company", "label": "Company",
"options": "Company", "options": "Company",
"reqd": 1 "reqd": 1,
"search_index": 1
}, },
{ {
"fieldname": "section_break_12", "fieldname": "section_break_12",
@@ -129,7 +130,7 @@
} }
], ],
"is_submittable": 1, "is_submittable": 1,
"modified": "2019-10-16 13:38:32.302316", "modified": "2019-11-18 19:37:37.151686",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Manufacturing", "module": "Manufacturing",
"name": "Blanket Order", "name": "Blanket Order",

View File

@@ -1,298 +1,78 @@
{ {
"allow_copy": 0, "creation": "2018-05-24 07:20:04.255236",
"allow_guest_to_view": 0, "doctype": "DocType",
"allow_import": 0, "editable_grid": 1,
"allow_rename": 0, "engine": "InnoDB",
"beta": 0, "field_order": [
"creation": "2018-05-24 07:20:04.255236", "item_code",
"custom": 0, "item_name",
"docstatus": 0, "column_break_3",
"doctype": "DocType", "qty",
"document_type": "", "rate",
"editable_grid": 1, "ordered_qty",
"engine": "InnoDB", "section_break_7",
"terms_and_conditions"
],
"fields": [ "fields": [
{ {
"allow_bulk_edit": 0, "fieldname": "item_code",
"allow_in_quick_entry": 0, "fieldtype": "Link",
"allow_on_submit": 0, "in_list_view": 1,
"bold": 0, "label": "Item Code",
"collapsible": 0, "options": "Item",
"columns": 0, "reqd": 1,
"fieldname": "item_code", "search_index": 1
"fieldtype": "Link", },
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Item Code",
"length": 0,
"no_copy": 0,
"options": "Item",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{ {
"allow_bulk_edit": 0, "fetch_from": "item_code.item_name",
"allow_in_quick_entry": 0, "fieldname": "item_name",
"allow_on_submit": 0, "fieldtype": "Data",
"bold": 0, "label": "Item Name"
"collapsible": 0, },
"columns": 0,
"fetch_from": "item_code.item_name",
"fieldname": "item_name",
"fieldtype": "Data",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Item Name",
"length": 0,
"no_copy": 0,
"options": "",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{ {
"allow_bulk_edit": 0, "fieldname": "column_break_3",
"allow_in_quick_entry": 0, "fieldtype": "Column Break"
"allow_on_submit": 0, },
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "column_break_3",
"fieldtype": "Column Break",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{ {
"allow_bulk_edit": 0, "fieldname": "qty",
"allow_in_quick_entry": 0, "fieldtype": "Float",
"allow_on_submit": 0, "in_list_view": 1,
"bold": 0, "label": "Quantity"
"collapsible": 0, },
"columns": 0,
"fieldname": "qty",
"fieldtype": "Float",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Quantity",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{ {
"allow_bulk_edit": 0, "fieldname": "rate",
"allow_in_quick_entry": 0, "fieldtype": "Currency",
"allow_on_submit": 0, "in_list_view": 1,
"bold": 0, "label": "Rate",
"collapsible": 0, "reqd": 1
"columns": 0, },
"fieldname": "rate",
"fieldtype": "Currency",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Rate",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{ {
"allow_bulk_edit": 0, "fieldname": "ordered_qty",
"allow_in_quick_entry": 0, "fieldtype": "Float",
"allow_on_submit": 0, "in_list_view": 1,
"bold": 0, "label": "Ordered Quantity",
"collapsible": 0, "no_copy": 1,
"columns": 0, "read_only": 1
"fieldname": "ordered_qty", },
"fieldtype": "Float",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Ordered Quantity",
"length": 0,
"no_copy": 1,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 1,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{ {
"allow_bulk_edit": 0, "fieldname": "section_break_7",
"allow_in_quick_entry": 0, "fieldtype": "Section Break"
"allow_on_submit": 0, },
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "section_break_7",
"fieldtype": "Section Break",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{ {
"allow_bulk_edit": 0, "fieldname": "terms_and_conditions",
"allow_in_quick_entry": 0, "fieldtype": "Text",
"allow_on_submit": 0, "label": "Terms and Conditions"
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "terms_and_conditions",
"fieldtype": "Text",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Terms and Conditions",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
} }
], ],
"has_web_view": 0, "istable": 1,
"hide_heading": 0, "modified": "2019-11-18 19:37:46.245878",
"hide_toolbar": 0, "modified_by": "Administrator",
"idx": 0, "module": "Manufacturing",
"image_view": 0, "name": "Blanket Order Item",
"in_create": 0, "owner": "Administrator",
"is_submittable": 0, "permissions": [],
"issingle": 0, "quick_entry": 1,
"istable": 1, "sort_field": "modified",
"max_attachments": 0, "sort_order": "DESC",
"modified": "2018-06-14 07:04:14.050836", "track_changes": 1
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "Blanket Order Item",
"name_case": "",
"owner": "Administrator",
"permissions": [],
"quick_entry": 1,
"read_only": 0,
"read_only_onload": 0,
"show_name_in_global_search": 0,
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1,
"track_seen": 0
} }

View File

@@ -5,6 +5,12 @@ frappe.provide("erpnext.bom");
frappe.ui.form.on("BOM", { frappe.ui.form.on("BOM", {
setup: function(frm) { setup: function(frm) {
frm.custom_make_buttons = {
'BOM': 'Duplicate BOM',
'Work Order': 'Work Order',
'Quality Inspection': 'Quality Inspection'
};
frm.set_query("bom_no", "items", function() { frm.set_query("bom_no", "items", function() {
return { return {
filters: { filters: {
@@ -85,9 +91,21 @@ frappe.ui.form.on("BOM", {
} }
if(frm.doc.docstatus!=0) { if(frm.doc.docstatus!=0) {
frm.add_custom_button(__("Duplicate"), function() { frm.add_custom_button(__("Duplicate BOM"), function() {
frm.copy_doc(); frm.copy_doc();
}); }, __("Create"));
frm.add_custom_button(__("Work Order"), function() {
frm.trigger("make_work_order");
}, __("Create"));
if (frm.doc.inspection_required) {
frm.add_custom_button(__("Quality Inspection"), function() {
frm.trigger("make_quality_inspection");
}, __("Create"));
}
frm.page.set_inner_btn_group_as_primary(__('Create'));
} }
if(frm.doc.items && frm.doc.allow_alternative_item) { if(frm.doc.items && frm.doc.allow_alternative_item) {
@@ -109,6 +127,41 @@ frappe.ui.form.on("BOM", {
} }
}, },
make_work_order: function(frm) {
const fields = [{
fieldtype: 'Float',
label: __('Qty To Manufacture'),
fieldname: 'qty',
reqd: 1,
default: 1
}];
frappe.prompt(fields, data => {
frappe.call({
method: "erpnext.manufacturing.doctype.work_order.work_order.make_work_order",
args: {
item: frm.doc.item,
qty: data.qty || 0.0,
project: frm.doc.project
},
freeze: true,
callback: function(r) {
if(r.message) {
var doc = frappe.model.sync(r.message)[0];
frappe.set_route("Form", doc.doctype, doc.name);
}
}
});
}, __("Enter Value"), __("Create"));
},
make_quality_inspection: function(frm) {
frappe.model.open_mapped_doc({
method: "erpnext.stock.doctype.quality_inspection.quality_inspection.make_quality_inspection",
frm: frm
})
},
update_cost: function(frm) { update_cost: function(frm) {
return frappe.call({ return frappe.call({
doc: frm.doc, doc: frm.doc,

View File

@@ -3,33 +3,36 @@
"creation": "2013-01-22 15:11:38", "creation": "2013-01-22 15:11:38",
"doctype": "DocType", "doctype": "DocType",
"document_type": "Setup", "document_type": "Setup",
"engine": "InnoDB",
"field_order": [ "field_order": [
"item", "item",
"item_name",
"image",
"uom",
"quantity", "quantity",
"set_rate_of_sub_assembly_item_based_on_bom",
"cb0", "cb0",
"is_active", "is_active",
"is_default", "is_default",
"with_operations",
"inspection_required",
"allow_alternative_item", "allow_alternative_item",
"allow_same_item_multiple_times", "image",
"set_rate_of_sub_assembly_item_based_on_bom", "item_name",
"quality_inspection_template", "uom",
"currency_detail", "currency_detail",
"company", "company",
"transfer_material_against", "project",
"conversion_rate", "conversion_rate",
"column_break_12", "column_break_12",
"currency", "currency",
"rm_cost_as_per", "rm_cost_as_per",
"buying_price_list", "buying_price_list",
"operations_section", "section_break_21",
"with_operations",
"column_break_23",
"transfer_material_against",
"routing", "routing",
"operations_section",
"operations", "operations",
"materials_section", "materials_section",
"inspection_required",
"quality_inspection_template",
"items", "items",
"scrap_section", "scrap_section",
"scrap_items", "scrap_items",
@@ -41,14 +44,9 @@
"base_operating_cost", "base_operating_cost",
"base_raw_material_cost", "base_raw_material_cost",
"base_scrap_material_cost", "base_scrap_material_cost",
"total_cost_of_bom",
"total_cost",
"column_break_26", "column_break_26",
"total_cost",
"base_total_cost", "base_total_cost",
"more_info_section",
"project",
"amended_from",
"col_break23",
"section_break_25", "section_break_25",
"description", "description",
"column_break_27", "column_break_27",
@@ -57,12 +55,14 @@
"website_section", "website_section",
"show_in_website", "show_in_website",
"route", "route",
"column_break_52",
"website_image", "website_image",
"thumbnail", "thumbnail",
"sb_web_spec", "sb_web_spec",
"web_long_description",
"show_items", "show_items",
"show_operations" "show_operations",
"web_long_description",
"amended_from"
], ],
"fields": [ "fields": [
{ {
@@ -152,7 +152,7 @@
"default": "0", "default": "0",
"fieldname": "inspection_required", "fieldname": "inspection_required",
"fieldtype": "Check", "fieldtype": "Check",
"label": "Inspection Required" "label": "Quality Inspection Required"
}, },
{ {
"default": "0", "default": "0",
@@ -160,12 +160,6 @@
"fieldtype": "Check", "fieldtype": "Check",
"label": "Allow Alternative Item" "label": "Allow Alternative Item"
}, },
{
"default": "0",
"fieldname": "allow_same_item_multiple_times",
"fieldtype": "Check",
"label": "Allow Same Item Multiple Times"
},
{ {
"allow_on_submit": 1, "allow_on_submit": 1,
"default": "1", "default": "1",
@@ -193,6 +187,7 @@
"reqd": 1 "reqd": 1
}, },
{ {
"default": "Work Order",
"fieldname": "transfer_material_against", "fieldname": "transfer_material_against",
"fieldtype": "Select", "fieldtype": "Select",
"label": "Transfer Material Against", "label": "Transfer Material Against",
@@ -235,10 +230,10 @@
{ {
"fieldname": "operations_section", "fieldname": "operations_section",
"fieldtype": "Section Break", "fieldtype": "Section Break",
"label": "Operations",
"oldfieldtype": "Section Break" "oldfieldtype": "Section Break"
}, },
{ {
"depends_on": "with_operations",
"fieldname": "routing", "fieldname": "routing",
"fieldtype": "Link", "fieldtype": "Link",
"label": "Routing", "label": "Routing",
@@ -335,10 +330,6 @@
"options": "Company:company:default_currency", "options": "Company:company:default_currency",
"read_only": 1 "read_only": 1
}, },
{
"fieldname": "total_cost_of_bom",
"fieldtype": "Section Break"
},
{ {
"fieldname": "total_cost", "fieldname": "total_cost",
"fieldtype": "Currency", "fieldtype": "Currency",
@@ -359,10 +350,6 @@
"print_hide": 1, "print_hide": 1,
"read_only": 1 "read_only": 1
}, },
{
"fieldname": "more_info_section",
"fieldtype": "Section Break"
},
{ {
"fieldname": "project", "fieldname": "project",
"fieldtype": "Link", "fieldtype": "Link",
@@ -381,10 +368,6 @@
"print_hide": 1, "print_hide": 1,
"read_only": 1 "read_only": 1
}, },
{
"fieldname": "col_break23",
"fieldtype": "Column Break"
},
{ {
"fieldname": "section_break_25", "fieldname": "section_break_25",
"fieldtype": "Section Break" "fieldtype": "Section Break"
@@ -481,13 +464,26 @@
"fieldname": "show_operations", "fieldname": "show_operations",
"fieldtype": "Check", "fieldtype": "Check",
"label": "Show Operations" "label": "Show Operations"
},
{
"fieldname": "section_break_21",
"fieldtype": "Section Break",
"label": "Operations"
},
{
"fieldname": "column_break_23",
"fieldtype": "Column Break"
},
{
"fieldname": "column_break_52",
"fieldtype": "Column Break"
} }
], ],
"icon": "fa fa-sitemap", "icon": "fa fa-sitemap",
"idx": 1, "idx": 1,
"image_field": "image", "image_field": "image",
"is_submittable": 1, "is_submittable": 1,
"modified": "2019-07-30 17:00:09.665068", "modified": "2019-11-22 14:35:12.142150",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Manufacturing", "module": "Manufacturing",
"name": "BOM", "name": "BOM",

View File

@@ -65,6 +65,7 @@ class BOM(WebsiteGenerator):
context.parents = [{'name': 'boms', 'title': _('All BOMs') }] context.parents = [{'name': 'boms', 'title': _('All BOMs') }]
def on_update(self): def on_update(self):
frappe.cache().hdel('bom_children', self.name)
self.check_recursion() self.check_recursion()
self.update_stock_qty() self.update_stock_qty()
self.update_exploded_items() self.update_exploded_items()
@@ -96,6 +97,7 @@ class BOM(WebsiteGenerator):
def get_routing(self): def get_routing(self):
if self.routing: if self.routing:
self.set("operations", [])
for d in frappe.get_all("BOM Operation", fields = ["*"], for d in frappe.get_all("BOM Operation", fields = ["*"],
filters = {'parenttype': 'Routing', 'parent': self.routing}): filters = {'parenttype': 'Routing', 'parent': self.routing}):
child = self.append('operations', d) child = self.append('operations', d)
@@ -289,7 +291,7 @@ class BOM(WebsiteGenerator):
if not valuation_rate: if not valuation_rate:
valuation_rate = frappe.db.get_value("Item", args['item_code'], "valuation_rate") valuation_rate = frappe.db.get_value("Item", args['item_code'], "valuation_rate")
return valuation_rate return flt(valuation_rate)
def manage_default_bom(self): def manage_default_bom(self):
""" Uncheck others if current one is selected as default or """ Uncheck others if current one is selected as default or
@@ -362,15 +364,9 @@ class BOM(WebsiteGenerator):
def validate_materials(self): def validate_materials(self):
""" Validate raw material entries """ """ Validate raw material entries """
def get_duplicates(lst):
seen = set()
seen_add = seen.add
for item in lst:
if item.item_code in seen or seen_add(item.item_code):
yield item
if not self.get('items'): if not self.get('items'):
frappe.throw(_("Raw Materials cannot be blank.")) frappe.throw(_("Raw Materials cannot be blank."))
check_list = [] check_list = []
for m in self.get('items'): for m in self.get('items'):
if m.bom_no: if m.bom_no:
@@ -379,16 +375,6 @@ class BOM(WebsiteGenerator):
frappe.throw(_("Quantity required for Item {0} in row {1}").format(m.item_code, m.idx)) frappe.throw(_("Quantity required for Item {0} in row {1}").format(m.item_code, m.idx))
check_list.append(m) check_list.append(m)
if not self.allow_same_item_multiple_times:
duplicate_items = list(get_duplicates(check_list))
if duplicate_items:
li = []
for i in duplicate_items:
li.append("{0} on row {1}".format(i.item_code, i.idx))
duplicate_list = '<br>' + '<br>'.join(li)
frappe.throw(_("Same item has been entered multiple times. {0}").format(duplicate_list))
def check_recursion(self, bom_list=[]): def check_recursion(self, bom_list=[]):
""" Check whether recursion occurs in any bom""" """ Check whether recursion occurs in any bom"""
bom_list = self.traverse_tree() bom_list = self.traverse_tree()

View File

@@ -17,11 +17,13 @@ def get_data():
}, },
{ {
'label': _('Manufacture'), 'label': _('Manufacture'),
'items': ['BOM', 'Work Order', 'Job Card', 'Production Plan'] 'items': ['BOM', 'Work Order', 'Job Card']
}, },
{ {
'label': _('Purchase'), 'label': _('Subcontract'),
'items': ['Purchase Order', 'Purchase Receipt', 'Purchase Invoice'] 'items': ['Purchase Order', 'Purchase Receipt', 'Purchase Invoice']
} }
] ],
'disable_create_buttons': ["Item", "Purchase Order", "Purchase Receipt",
"Purchase Invoice", "Job Card", "Stock Entry"]
} }

File diff suppressed because it is too large Load Diff

View File

@@ -9,6 +9,7 @@ from frappe import _
from six import string_types from six import string_types
from erpnext.manufacturing.doctype.bom.bom import get_boms_in_bottom_up_order from erpnext.manufacturing.doctype.bom.bom import get_boms_in_bottom_up_order
from frappe.model.document import Document from frappe.model.document import Document
import click
class BOMUpdateTool(Document): class BOMUpdateTool(Document):
def replace_bom(self): def replace_bom(self):
@@ -17,7 +18,8 @@ class BOMUpdateTool(Document):
frappe.cache().delete_key('bom_children') frappe.cache().delete_key('bom_children')
bom_list = self.get_parent_boms(self.new_bom) bom_list = self.get_parent_boms(self.new_bom)
updated_bom = [] updated_bom = []
with click.progressbar(bom_list) as bom_list:
pass
for bom in bom_list: for bom in bom_list:
try: try:
bom_obj = frappe.get_cached_doc('BOM', bom) bom_obj = frappe.get_cached_doc('BOM', bom)

View File

@@ -529,7 +529,6 @@ def get_material_request_items(row, sales_order,
required_qty = ceil(required_qty) required_qty = ceil(required_qty)
if required_qty > 0: if required_qty > 0:
print(row)
return { return {
'item_code': row.item_code, 'item_code': row.item_code,
'item_name': row.item_name, 'item_name': row.item_name,

View File

@@ -581,6 +581,8 @@ erpnext.work_order = {
description: __('Max: {0}', [max]), description: __('Max: {0}', [max]),
default: max default: max
}, data => { }, data => {
max += (max * (frm.doc.__onload.overproduction_percentage || 0.0)) / 100;
if (data.qty > max) { if (data.qty > max) {
frappe.msgprint(__('Quantity must not be more than {0}', [max])); frappe.msgprint(__('Quantity must not be more than {0}', [max]));
reject(); reject();

View File

@@ -37,7 +37,7 @@ class WorkOrder(Document):
ms = frappe.get_doc("Manufacturing Settings") ms = frappe.get_doc("Manufacturing Settings")
self.set_onload("material_consumption", ms.material_consumption) self.set_onload("material_consumption", ms.material_consumption)
self.set_onload("backflush_raw_materials_based_on", ms.backflush_raw_materials_based_on) self.set_onload("backflush_raw_materials_based_on", ms.backflush_raw_materials_based_on)
self.set_onload("overproduction_percentage", ms.overproduction_percentage_for_work_order)
def validate(self): def validate(self):
self.validate_production_item() self.validate_production_item()
@@ -609,6 +609,23 @@ def get_item_details(item, project = None):
return res return res
@frappe.whitelist()
def make_work_order(item, qty=0, project=None):
if not frappe.has_permission("Work Order", "write"):
frappe.throw(_("Not permitted"), frappe.PermissionError)
item_details = get_item_details(item, project)
wo_doc = frappe.new_doc("Work Order")
wo_doc.production_item = item
wo_doc.update(item_details)
if flt(qty) > 0:
wo_doc.qty = flt(qty)
wo_doc.get_items_and_operations_from_bom()
return wo_doc
@frappe.whitelist() @frappe.whitelist()
def check_if_scrap_warehouse_mandatory(bom_no): def check_if_scrap_warehouse_mandatory(bom_no):
res = {"set_scrap_wh_mandatory": False } res = {"set_scrap_wh_mandatory": False }

View File

@@ -645,4 +645,6 @@ erpnext.patches.v12_0.replace_accounting_with_accounts_in_home_settings
erpnext.patches.v12_0.set_payment_entry_status erpnext.patches.v12_0.set_payment_entry_status
erpnext.patches.v12_0.update_owner_fields_in_acc_dimension_custom_fields erpnext.patches.v12_0.update_owner_fields_in_acc_dimension_custom_fields
erpnext.patches.v12_0.set_default_for_add_taxes_from_item_tax_template erpnext.patches.v12_0.set_default_for_add_taxes_from_item_tax_template
erpnext.patches.v12_0.remove_denied_leaves_from_leave_ledger erpnext.patches.v12_0.remove_denied_leaves_from_leave_ledger
erpnext.patches.v12_0.update_price_or_product_discount
erpnext.patches.v12_0.add_export_type_field_in_party_master

View File

@@ -15,13 +15,6 @@ def execute():
rename_field(doctype, "allow_transfer_for_manufacture", "include_item_in_manufacturing") rename_field(doctype, "allow_transfer_for_manufacture", "include_item_in_manufacturing")
if frappe.db.has_column('BOM', 'allow_same_item_multiple_times'):
frappe.db.sql(""" UPDATE tabBOM
SET
allow_same_item_multiple_times = 0
WHERE
trim(coalesce(allow_same_item_multiple_times, '')) = '' """)
for doctype in ['BOM', 'Work Order']: for doctype in ['BOM', 'Work Order']:
frappe.reload_doc('manufacturing', 'doctype', frappe.scrub(doctype)) frappe.reload_doc('manufacturing', 'doctype', frappe.scrub(doctype))

View File

@@ -0,0 +1,40 @@
from __future__ import unicode_literals
import frappe
from erpnext.regional.india.setup import make_custom_fields
def execute():
company = frappe.get_all('Company', filters = {'country': 'India'})
if not company:
return
make_custom_fields()
frappe.reload_doctype('Tax Category')
frappe.reload_doctype('Sales Taxes and Charges Template')
frappe.reload_doctype('Purchase Taxes and Charges Template')
# Create tax category with inter state field checked
tax_category = frappe.db.get_value('Tax Category', {'name': 'OUT OF STATE'}, 'name')
if not tax_category:
inter_state_category = frappe.get_doc({
'doctype': 'Tax Category',
'title': 'OUT OF STATE',
'is_inter_state': 1
}).insert()
tax_category = inter_state_category.name
for doctype in ('Sales Taxes and Charges Template', 'Purchase Taxes and Charges Template'):
template = frappe.db.get_value(doctype, {'is_inter_state': 1, 'disabled': 0}, ['name'])
if template:
frappe.db.set_value(doctype, template, 'tax_category', tax_category)
frappe.db.sql("""
DELETE FROM `tabCustom Field`
WHERE fieldname = 'is_inter_state'
AND dt IN ('Sales Taxes and Charges Template', 'Purchase Taxes and Charges Template')
""")

View File

@@ -62,12 +62,12 @@ def execute():
] ]
for dt in doctypes: for dt in doctypes:
for d in frappe.db.sql("""select name, parent, item_code, item_tax_rate from `tab{0} Item` for d in frappe.db.sql("""select name, parenttype, parent, item_code, item_tax_rate from `tab{0} Item`
where ifnull(item_tax_rate, '') not in ('', '{{}}') where ifnull(item_tax_rate, '') not in ('', '{{}}')
and item_tax_template is NULL""".format(dt), as_dict=1): and item_tax_template is NULL""".format(dt), as_dict=1):
item_tax_map = json.loads(d.item_tax_rate) item_tax_map = json.loads(d.item_tax_rate)
item_tax_template_name = get_item_tax_template(item_tax_templates, item_tax_template_name = get_item_tax_template(item_tax_templates,
item_tax_map, d.item_code, d.parent) item_tax_map, d.item_code, d.parenttype, d.parent)
frappe.db.set_value(dt + " Item", d.name, "item_tax_template", item_tax_template_name) frappe.db.set_value(dt + " Item", d.name, "item_tax_template", item_tax_template_name)
frappe.db.auto_commit_on_many_writes = False frappe.db.auto_commit_on_many_writes = False
@@ -77,7 +77,7 @@ def execute():
settings.determine_address_tax_category_from = "Billing Address" settings.determine_address_tax_category_from = "Billing Address"
settings.save() settings.save()
def get_item_tax_template(item_tax_templates, item_tax_map, item_code, parent=None): def get_item_tax_template(item_tax_templates, item_tax_map, item_code, parenttype=None, parent=None):
# search for previously created item tax template by comparing tax maps # search for previously created item tax template by comparing tax maps
for template, item_tax_template_map in iteritems(item_tax_templates): for template, item_tax_template_map in iteritems(item_tax_templates):
if item_tax_map == item_tax_template_map: if item_tax_map == item_tax_template_map:
@@ -88,23 +88,44 @@ def get_item_tax_template(item_tax_templates, item_tax_map, item_code, parent=No
item_tax_template.title = make_autoname("Item Tax Template-.####") item_tax_template.title = make_autoname("Item Tax Template-.####")
for tax_type, tax_rate in iteritems(item_tax_map): for tax_type, tax_rate in iteritems(item_tax_map):
if not frappe.db.exists("Account", tax_type): account_details = frappe.db.get_value("Account", tax_type, ['name', 'account_type'], as_dict=1)
if account_details:
if account_details.account_type not in ('Tax', 'Chargeable', 'Income Account', 'Expense Account', 'Expenses Included In Valuation'):
frappe.db.set_value('Account', account_details.name, 'account_type', 'Chargeable')
else:
parts = tax_type.strip().split(" - ") parts = tax_type.strip().split(" - ")
account_name = " - ".join(parts[:-1]) account_name = " - ".join(parts[:-1])
company = frappe.db.get_value("Company", filters={"abbr": parts[-1]}) company = get_company(parts[-1], parenttype, parent)
parent_account = frappe.db.get_value("Account", parent_account = frappe.db.get_value("Account",
filters={"account_type": "Tax", "root_type": "Liability", "is_group": 0, "company": company}, fieldname="parent_account") filters={"account_type": "Tax", "root_type": "Liability", "is_group": 0, "company": company}, fieldname="parent_account")
filters = {
frappe.get_doc({
"doctype": "Account",
"account_name": account_name, "account_name": account_name,
"company": company, "company": company,
"account_type": "Tax", "account_type": "Tax",
"parent_account": parent_account "parent_account": parent_account
}).insert() }
tax_type = frappe.db.get_value("Account", filters)
if not tax_type:
account = frappe.new_doc("Account")
account.update(filters)
account.insert()
tax_type = account.name
item_tax_template.append("taxes", {"tax_type": tax_type, "tax_rate": tax_rate}) item_tax_template.append("taxes", {"tax_type": tax_type, "tax_rate": tax_rate})
item_tax_templates.setdefault(item_tax_template.title, {}) item_tax_templates.setdefault(item_tax_template.title, {})
item_tax_templates[item_tax_template.title][tax_type] = tax_rate item_tax_templates[item_tax_template.title][tax_type] = tax_rate
item_tax_template.save() item_tax_template.save()
return item_tax_template.name return item_tax_template.name
def get_company(company_abbr, parenttype=None, parent=None):
if parenttype and parent:
company = frappe.get_cached_value(parenttype, parent, 'company')
else:
company = frappe.db.get_value("Company", filters={"abbr": company_abbr})
if not company:
companies = frappe.get_all('Company')
if len(companies) == 1:
company = companies[0].name
return company

View File

@@ -7,6 +7,8 @@ def execute():
if not company: if not company:
return return
frappe.reload_doc('accounts', 'doctype', 'Tax Category')
make_custom_fields() make_custom_fields()
for doctype in ['Sales Invoice', 'Purchase Invoice']: for doctype in ['Sales Invoice', 'Purchase Invoice']:

View File

@@ -0,0 +1,8 @@
from __future__ import unicode_literals
import frappe
def execute():
frappe.reload_doc("accounts", "doctype", "pricing_rule")
frappe.db.sql(""" UPDATE `tabPricing Rule` SET price_or_product_discount = 'Price'
WHERE ifnull(price_or_product_discount,'') = '' """)

View File

@@ -302,6 +302,8 @@ def get_items(filters=None, search=None):
if isinstance(filters, dict): if isinstance(filters, dict):
filters = [['Item', fieldname, '=', value] for fieldname, value in filters.items()] filters = [['Item', fieldname, '=', value] for fieldname, value in filters.items()]
enabled_items_filter = get_conditions({ 'disabled': 0 }, 'and')
show_in_website_condition = '' show_in_website_condition = ''
if products_settings.hide_variants: if products_settings.hide_variants:
show_in_website_condition = get_conditions({'show_in_website': 1 }, 'and') show_in_website_condition = get_conditions({'show_in_website': 1 }, 'and')
@@ -313,19 +315,32 @@ def get_items(filters=None, search=None):
search_condition = '' search_condition = ''
if search: if search:
# Default fields to search from
default_fields = {'name', 'item_name', 'description', 'item_group'}
# Get meta search fields
meta = frappe.get_meta("Item")
meta_fields = set(meta.get_search_fields())
# Join the meta fields and default fields set
search_fields = default_fields.union(meta_fields)
try:
if frappe.db.count('Item', cache=True) > 50000:
search_fields.remove('description')
except KeyError:
pass
# Build or filters for query
search = '%{}%'.format(search) search = '%{}%'.format(search)
or_filters = [ or_filters = [[field, 'like', search] for field in search_fields]
['name', 'like', search],
['item_name', 'like', search],
['description', 'like', search],
['item_group', 'like', search]
]
search_condition = get_conditions(or_filters, 'or') search_condition = get_conditions(or_filters, 'or')
filter_condition = get_conditions(filters, 'and') filter_condition = get_conditions(filters, 'and')
where_conditions = ' and '.join( where_conditions = ' and '.join(
[condition for condition in [show_in_website_condition, search_condition, filter_condition] if condition] [condition for condition in [enabled_items_filter, show_in_website_condition, \
search_condition, filter_condition] if condition]
) )
left_joins = [] left_joins = []

View File

@@ -4,20 +4,16 @@ frappe.ui.form.on("Project", {
setup(frm) { setup(frm) {
frm.make_methods = { frm.make_methods = {
'Timesheet': () => { 'Timesheet': () => {
let doctype = 'Timesheet'; open_form(frm, "Timesheet", "Timesheet Detail", "time_logs");
frappe.model.with_doctype(doctype, () => { },
let new_doc = frappe.model.get_new_doc(doctype); 'Purchase Order': () => {
open_form(frm, "Purchase Order", "Purchase Order Item", "items");
// add a new row and set the project },
let time_log = frappe.model.get_new_doc('Timesheet Detail'); 'Purchase Receipt': () => {
time_log.project = frm.doc.name; open_form(frm, "Purchase Receipt", "Purchase Receipt Item", "items");
time_log.parent = new_doc.name; },
time_log.parentfield = 'time_logs'; 'Purchase Invoice': () => {
time_log.parenttype = 'Timesheet'; open_form(frm, "Purchase Invoice", "Purchase Invoice Item", "items");
new_doc.time_logs = [time_log];
frappe.ui.form.make_quick_entry(doctype, null, null, new_doc);
});
}, },
}; };
}, },
@@ -80,7 +76,7 @@ frappe.ui.form.on("Project", {
frm.events.set_status(frm, 'Cancelled'); frm.events.set_status(frm, 'Cancelled');
}, __('Set Status')); }, __('Set Status'));
} }
if (frappe.model.can_read("Task")) { if (frappe.model.can_read("Task")) {
frm.add_custom_button(__("Gantt Chart"), function () { frm.add_custom_button(__("Gantt Chart"), function () {
frappe.route_options = { frappe.route_options = {
@@ -123,3 +119,20 @@ frappe.ui.form.on("Project", {
}, },
}); });
function open_form(frm, doctype, child_doctype, parentfield) {
frappe.model.with_doctype(doctype, () => {
let new_doc = frappe.model.get_new_doc(doctype);
// add a new row and set the project
let new_child_doc = frappe.model.get_new_doc(child_doctype);
new_child_doc.project = frm.doc.name;
new_child_doc.parent = new_doc.name;
new_child_doc.parentfield = parentfield;
new_child_doc.parenttype = doctype;
new_doc[parentfield] = [new_child_doc];
frappe.ui.form.make_quick_entry(doctype, null, null, new_doc);
});
}

View File

@@ -47,11 +47,11 @@ class Task(NestedSet):
if not self.project or frappe.flags.in_test: if not self.project or frappe.flags.in_test:
return return
expected_end_date = getdate(frappe.db.get_value("Project", self.project, "expected_end_date")) expected_end_date = frappe.db.get_value("Project", self.project, "expected_end_date")
if expected_end_date: if expected_end_date:
validate_project_dates(expected_end_date, self, "exp_start_date", "exp_end_date", "Expected") validate_project_dates(getdate(expected_end_date), self, "exp_start_date", "exp_end_date", "Expected")
validate_project_dates(expected_end_date, self, "act_start_date", "act_end_date", "Actual") validate_project_dates(getdate(expected_end_date), self, "act_start_date", "act_end_date", "Actual")
def validate_status(self): def validate_status(self):
if self.status!=self.get_db_value("status") and self.status == "Completed": if self.status!=self.get_db_value("status") and self.status == "Completed":
@@ -278,4 +278,4 @@ def validate_project_dates(project_end_date, task, task_start, task_end, actual_
frappe.throw(_("Task's {0} Start Date cannot be after Project's End Date.").format(actual_or_expected_date)) frappe.throw(_("Task's {0} Start Date cannot be after Project's End Date.").format(actual_or_expected_date))
if task.get(task_end) and date_diff(project_end_date, getdate(task.get(task_end))) < 0: if task.get(task_end) and date_diff(project_end_date, getdate(task.get(task_end))) < 0:
frappe.throw(_("Task's {0} End Date cannot be after Project's End Date.").format(actual_or_expected_date)) frappe.throw(_("Task's {0} End Date cannot be after Project's End Date.").format(actual_or_expected_date))

View File

@@ -188,7 +188,8 @@ class Timesheet(Document):
}, as_dict=True) }, as_dict=True)
# check internal overlap # check internal overlap
for time_log in self.time_logs: for time_log in self.time_logs:
if not (time_log.from_time or time_log.to_time): continue if not (time_log.from_time and time_log.to_time
and args.from_time and args.to_time): continue
if (fieldname != 'workstation' or args.get(fieldname) == time_log.get(fieldname)) and \ if (fieldname != 'workstation' or args.get(fieldname) == time_log.get(fieldname)) and \
args.idx != time_log.idx and ((args.from_time > time_log.from_time and args.from_time < time_log.to_time) or args.idx != time_log.idx and ((args.from_time > time_log.from_time and args.from_time < time_log.to_time) or

Some files were not shown because too many files have changed in this diff Show More