Merge branch 'develop' of https://github.com/frappe/erpnext into develop

This commit is contained in:
Khushal Trivedi 2019-11-26 15:33:17 +05:30
commit 529a5f84bb
87 changed files with 4027 additions and 7797 deletions

View File

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

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
outstanding_amount = doc.outstanding_amount
elif dt in ("Expense Claim"):
grand_total = doc.total_sanctioned_amount
outstanding_amount = doc.total_sanctioned_amount \
- doc.total_amount_reimbursed - flt(doc.total_advance_amount)
grand_total = doc.total_sanctioned_amount + doc.total_taxes_and_charges
outstanding_amount = doc.grand_total \
- doc.total_amount_reimbursed
elif dt == "Employee Advance":
grand_total = doc.advance_amount
outstanding_amount = flt(doc.advance_amount) - flt(doc.paid_amount)

View File

@ -237,7 +237,7 @@ class PurchaseInvoice(BuyingController):
item.expense_account = warehouse_account[item.warehouse]["account"]
else:
item.expense_account = stock_not_billed_account
elif item.is_fixed_asset and not is_cwip_accounting_enabled(self.company, asset_category):
elif item.is_fixed_asset and not is_cwip_accounting_enabled(asset_category):
item.expense_account = get_asset_category_account('fixed_asset_account', item=item.item_code,
company = self.company)
elif item.is_fixed_asset and item.pr_detail:
@ -357,7 +357,7 @@ class PurchaseInvoice(BuyingController):
return
if not gl_entries:
gl_entries = self.get_gl_entries()
if gl_entries:
update_outstanding = "No" if (cint(self.is_paid) or self.write_off_account) else "Yes"
@ -408,7 +408,7 @@ class PurchaseInvoice(BuyingController):
for item in self.get("items"):
if item.item_code and item.is_fixed_asset:
asset_category = frappe.get_cached_value("Item", item.item_code, "asset_category")
if is_cwip_accounting_enabled(self.company, asset_category):
if is_cwip_accounting_enabled(asset_category):
return 1
return 0
@ -452,6 +452,10 @@ class PurchaseInvoice(BuyingController):
fields = ["voucher_detail_no", "stock_value_difference"], filters={'voucher_no': self.name}):
voucher_wise_stock_value.setdefault(d.voucher_detail_no, d.stock_value_difference)
valuation_tax_accounts = [d.account_head for d in self.get("taxes")
if d.category in ('Valuation', 'Total and Valuation')
and flt(d.base_tax_amount_after_discount_amount)]
for item in self.get("items"):
if flt(item.base_net_amount):
account_currency = get_account_currency(item.expense_account)
@ -500,11 +504,10 @@ class PurchaseInvoice(BuyingController):
"credit": flt(item.rm_supp_cost)
}, warehouse_account[self.supplier_warehouse]["account_currency"], item=item))
elif not item.is_fixed_asset or (item.is_fixed_asset and not is_cwip_accounting_enabled(self.company,
asset_category)):
elif not item.is_fixed_asset or (item.is_fixed_asset and not is_cwip_accounting_enabled(asset_category)):
expense_account = (item.expense_account
if (not item.enable_deferred_expense or self.is_return) else item.deferred_expense_account)
if not item.is_fixed_asset:
amount = flt(item.base_net_amount, item.precision("base_net_amount"))
else:
@ -517,7 +520,7 @@ class PurchaseInvoice(BuyingController):
"cost_center": item.cost_center,
"project": item.project
}, account_currency, item=item))
# If asset is bought through this document and not linked to PR
if self.update_stock and item.landed_cost_voucher_amount:
expenses_included_in_asset_valuation = self.get_company_default("expenses_included_in_asset_valuation")
@ -539,9 +542,9 @@ class PurchaseInvoice(BuyingController):
"debit": flt(item.landed_cost_voucher_amount),
"project": item.project
}, item=item))
# update gross amount of asset bought through this document
assets = frappe.db.get_all('Asset',
assets = frappe.db.get_all('Asset',
filters={ 'purchase_invoice': self.name, 'item_code': item.item_code }
)
for asset in assets:
@ -551,10 +554,10 @@ class PurchaseInvoice(BuyingController):
if self.auto_accounting_for_stock and self.is_opening == "No" and \
item.item_code in stock_items and item.item_tax_amount:
# Post reverse entry for Stock-Received-But-Not-Billed if it is booked in Purchase Receipt
if item.purchase_receipt:
if item.purchase_receipt and valuation_tax_accounts:
negative_expense_booked_in_pr = frappe.db.sql("""select name from `tabGL Entry`
where voucher_type='Purchase Receipt' and voucher_no=%s and account=%s""",
(item.purchase_receipt, self.expenses_included_in_valuation))
where voucher_type='Purchase Receipt' and voucher_no=%s and account in %s""",
(item.purchase_receipt, valuation_tax_accounts))
if not negative_expense_booked_in_pr:
gl_entries.append(
@ -633,7 +636,7 @@ class PurchaseInvoice(BuyingController):
if asset_eiiav_currency == self.company_currency else
item.item_tax_amount / self.conversion_rate)
}, item=item))
# When update stock is checked
# Assets are bought through this document then it will be linked to this document
if self.update_stock:
@ -655,9 +658,9 @@ class PurchaseInvoice(BuyingController):
"debit": flt(item.landed_cost_voucher_amount),
"project": item.project
}, item=item))
# update gross amount of assets bought through this document
assets = frappe.db.get_all('Asset',
assets = frappe.db.get_all('Asset',
filters={ 'purchase_invoice': self.name, 'item_code': item.item_code }
)
for asset in assets:

View File

@ -204,7 +204,7 @@ class TestPurchaseInvoice(unittest.TestCase):
pi.insert()
pi.submit()
self.check_gle_for_pi_against_pr(pi.name)
self.check_gle_for_pi(pi.name)
def check_gle_for_pi(self, pi):
gl_entries = frappe.db.sql("""select account, sum(debit) as debit, sum(credit) as credit
@ -225,26 +225,6 @@ class TestPurchaseInvoice(unittest.TestCase):
self.assertEqual(expected_values[gle.account][1], gle.debit)
self.assertEqual(expected_values[gle.account][2], gle.credit)
def check_gle_for_pi_against_pr(self, pi):
gl_entries = frappe.db.sql("""select account, sum(debit) as debit, sum(credit) as credit
from `tabGL Entry` where voucher_type='Purchase Invoice' and voucher_no=%s
group by account""", pi, as_dict=1)
self.assertTrue(gl_entries)
expected_values = dict((d[0], d) for d in [
["Creditors - TCP1", 0, 720],
["Stock Received But Not Billed - TCP1", 750.0, 0],
["_Test Account Shipping Charges - TCP1", 100.0, 100.0],
["_Test Account VAT - TCP1", 120.0, 0],
["_Test Account Customs Duty - TCP1", 0, 150]
])
for i, gle in enumerate(gl_entries):
self.assertEqual(expected_values[gle.account][0], gle.account)
self.assertEqual(expected_values[gle.account][1], gle.debit)
self.assertEqual(expected_values[gle.account][2], gle.credit)
def test_purchase_invoice_change_naming_series(self):
pi = frappe.copy_doc(test_records[1])
pi.insert()

View File

@ -117,6 +117,7 @@
},
{
"fetch_from": "item_code.item_name",
"fetch_if_empty": 1,
"fieldname": "item_name",
"fieldtype": "Data",
"in_global_search": 1,
@ -192,7 +193,6 @@
"fieldtype": "Column Break"
},
{
"fetch_from": "item_code.stock_uom",
"fieldname": "uom",
"fieldtype": "Link",
"label": "UOM",
@ -766,7 +766,7 @@
],
"idx": 1,
"istable": 1,
"modified": "2019-11-03 13:43:23.782877",
"modified": "2019-11-21 16:27:52.043744",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Purchase Invoice Item",

View File

@ -135,7 +135,7 @@ class SalesInvoice(SellingController):
if self.redeem_loyalty_points and self.loyalty_program and self.loyalty_points:
validate_loyalty_points(self, self.loyalty_points)
def validate_fixed_asset(self):
for d in self.get("items"):
if d.is_fixed_asset and d.meta.get_field("asset") and d.asset:

View File

@ -13,9 +13,9 @@ from frappe.utils import nowdate
class ShareDontExists(ValidationError): pass
class ShareTransfer(Document):
def before_submit(self):
def on_submit(self):
if self.transfer_type == 'Issue':
shareholder = self.get_shareholder_doc(self.company)
shareholder = self.get_company_shareholder()
shareholder.append('share_balance', {
'share_type': self.share_type,
'from_no': self.from_no,
@ -28,7 +28,7 @@ class ShareTransfer(Document):
})
shareholder.save()
doc = frappe.get_doc('Shareholder', self.to_shareholder)
doc = self.get_shareholder_doc(self.to_shareholder)
doc.append('share_balance', {
'share_type': self.share_type,
'from_no': self.from_no,
@ -41,11 +41,11 @@ class ShareTransfer(Document):
elif self.transfer_type == 'Purchase':
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':
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', {
'share_type': self.share_type,
'from_no': self.from_no,
@ -56,143 +56,127 @@ class ShareTransfer(Document):
})
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):
self.get_company_shareholder()
self.basic_validations()
self.folio_no_validation()
if self.transfer_type == 'Issue':
if not self.get_shareholder_doc(self.company):
shareholder = frappe.get_doc({
'doctype': 'Shareholder',
'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:
# validate share doesn't exist in company
ret_val = self.share_exists(self.get_company_shareholder().name)
if ret_val in ('Complete', 'Partial'):
frappe.throw(_('The shares already exist'), frappe.DuplicateEntryError)
else:
# validate share exists with 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}")
.format(self.from_shareholder), ShareDontExists)
def basic_validations(self):
if self.transfer_type == 'Purchase':
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'))
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)
if self.asset_account is None:
if not self.asset_account:
frappe.throw(_('The field Asset Account cannot be blank'))
elif (self.transfer_type == 'Issue'):
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'))
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)
if self.asset_account is None:
if not self.asset_account:
frappe.throw(_('The field Asset Account cannot be blank'))
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'))
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)
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'))
if self.from_shareholder == self.to_shareholder:
frappe.throw(_('The seller and the buyer cannot be the same'))
if self.no_of_shares != self.to_no - self.from_no + 1:
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
if self.amount != self.rate * self.no_of_shares:
frappe.throw(_('There are inconsistencies between the rate, no of shares and the amount calculated'))
def share_exists(self, shareholder):
# return True if exits,
# 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)
doc = self.get_shareholder_doc(shareholder)
for entry in doc.share_balance:
if entry.share_type != share_type or \
entry.from_no > query['to_no'] or \
entry.to_no < query['from_no']:
if entry.share_type != self.share_type or \
entry.from_no > self.to_no or \
entry.to_no < self.from_no:
continue # since query lies outside bounds
elif entry.from_no <= query['from_no'] and entry.to_no >= query['to_no']:
return [True] # absolute truth!
elif entry.from_no >= query['from_no'] and entry.to_no <= query['to_no']:
# split and check
does_share_exist.extend(self.recursive_share_check(shareholder,
share_type,
{
'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']
}
))
elif entry.from_no <= self.from_no and entry.to_no >= self.to_no: #both inside
return 'Complete' # absolute truth!
elif entry.from_no <= self.from_no <= self.to_no:
return 'Partial'
elif entry.from_no <= self.to_no <= entry.to_no:
return 'Partial'
does_share_exist.append(False)
return does_share_exist
return 'Outside'
def folio_no_validation(self):
shareholders = ['from_shareholder', 'to_shareholder']
shareholders = [shareholder for shareholder in shareholders if self.get(shareholder) is not '']
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:
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 \
if (shareholder == 'from_shareholder') else self.to_folio_no;
if (shareholder == 'from_shareholder') else self.to_folio_no
doc.save()
else:
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):
if is_company:
doc = self.get_shareholder_doc(shareholder)
doc = self.get_company_shareholder()
else:
doc = frappe.get_doc('Shareholder' , shareholder)
doc = self.get_shareholder_doc(shareholder)
doc.folio_no = make_autoname('FN.#####')
doc.save()
return doc.folio_no
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}
# Shares exist for sure
# Iterate over all entries and modify entry if in entry
@ -227,31 +201,31 @@ class ShareTransfer(Document):
for entry in current_entries:
# use spaceage logic here
if entry.share_type != share_type or \
entry.from_no > query['to_no'] or \
entry.to_no < query['from_no']:
if entry.share_type != self.share_type or \
entry.from_no > self.to_no or \
entry.to_no < self.from_no:
new_entries.append(entry)
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
if entry.from_no == query['from_no']:
if entry.to_no == query['to_no']:
if entry.from_no == self.from_no:
if entry.to_no == self.to_no:
pass #nothing to append
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:
if entry.to_no == query['to_no']:
new_entries.append(self.return_share_balance_entry(entry.from_no, query['from_no']-1, entry.rate))
if entry.to_no == self.to_no:
new_entries.append(self.return_share_balance_entry(entry.from_no, self.from_no-1, entry.rate))
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(query['to_no']+1, entry.to_no, entry.rate))
elif entry.from_no >= query['from_no'] and entry.to_no <= query['to_no']:
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(self.to_no+1, entry.to_no, entry.rate))
elif entry.from_no >= self.from_no and entry.to_no <= self.to_no:
# split and check
pass #nothing to append
elif query['from_no'] <= entry.from_no <= query['to_no'] and entry.to_no >= query['to_no']:
new_entries.append(self.return_share_balance_entry(query['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']:
new_entries.append(self.return_share_balance_entry(entry.from_no, query['from_no']-1, entry.rate))
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(self.to_no+1, entry.to_no, entry.rate))
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, self.from_no-1, entry.rate))
else:
new_entries.append(entry)
@ -272,16 +246,34 @@ class ShareTransfer(Document):
}
def get_shareholder_doc(self, shareholder):
# Get Shareholder doc based on the Shareholder title
doc = frappe.get_list('Shareholder',
filters = [
('Shareholder', 'title', '=', shareholder)
]
)
if len(doc) == 1:
return frappe.get_doc('Shareholder', doc[0]['name'])
else: #It will necessarily by 0 indicating it doesn't exist
return False
# Get Shareholder doc based on the Shareholder name
if shareholder:
query_filters = {'name': shareholder}
name = frappe.db.get_value('Shareholder', {'name': shareholder}, 'name')
return frappe.get_doc('Shareholder', name)
def get_company_shareholder(self):
# 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()
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`")
share_transfers = [
{
"doctype" : "Share Transfer",
"transfer_type" : "Issue",
"date" : "2018-01-01",
"to_shareholder" : "SH-00001",
"share_type" : "Equity",
"from_no" : 1,
"to_no" : 500,
"no_of_shares" : 500,
"rate" : 10,
"company" : "_Test Company",
"asset_account" : "Cash - _TC",
"doctype": "Share Transfer",
"transfer_type": "Issue",
"date": "2018-01-01",
"to_shareholder": "SH-00001",
"share_type": "Equity",
"from_no": 1,
"to_no": 500,
"no_of_shares": 500,
"rate": 10,
"company": "_Test Company",
"asset_account": "Cash - _TC",
"equity_or_liability_account": "Creditors - _TC"
},
{
"doctype" : "Share Transfer",
"transfer_type" : "Transfer",
"date" : "2018-01-02",
"from_shareholder" : "SH-00001",
"to_shareholder" : "SH-00002",
"share_type" : "Equity",
"from_no" : 101,
"to_no" : 200,
"no_of_shares" : 100,
"rate" : 15,
"company" : "_Test Company",
"doctype": "Share Transfer",
"transfer_type": "Transfer",
"date": "2018-01-02",
"from_shareholder": "SH-00001",
"to_shareholder": "SH-00002",
"share_type": "Equity",
"from_no": 101,
"to_no": 200,
"no_of_shares": 100,
"rate": 15,
"company": "_Test Company",
"equity_or_liability_account": "Creditors - _TC"
},
{
"doctype" : "Share Transfer",
"transfer_type" : "Transfer",
"date" : "2018-01-03",
"from_shareholder" : "SH-00001",
"to_shareholder" : "SH-00003",
"share_type" : "Equity",
"from_no" : 201,
"to_no" : 500,
"no_of_shares" : 300,
"rate" : 20,
"company" : "_Test Company",
"doctype": "Share Transfer",
"transfer_type": "Transfer",
"date": "2018-01-03",
"from_shareholder": "SH-00001",
"to_shareholder": "SH-00003",
"share_type": "Equity",
"from_no": 201,
"to_no": 500,
"no_of_shares": 300,
"rate": 20,
"company": "_Test Company",
"equity_or_liability_account": "Creditors - _TC"
},
{
"doctype" : "Share Transfer",
"transfer_type" : "Transfer",
"date" : "2018-01-04",
"from_shareholder" : "SH-00003",
"to_shareholder" : "SH-00002",
"share_type" : "Equity",
"from_no" : 201,
"to_no" : 400,
"no_of_shares" : 200,
"rate" : 15,
"company" : "_Test Company",
"doctype": "Share Transfer",
"transfer_type": "Transfer",
"date": "2018-01-04",
"from_shareholder": "SH-00003",
"to_shareholder": "SH-00002",
"share_type": "Equity",
"from_no": 201,
"to_no": 400,
"no_of_shares": 200,
"rate": 15,
"company": "_Test Company",
"equity_or_liability_account": "Creditors - _TC"
},
{
"doctype" : "Share Transfer",
"transfer_type" : "Purchase",
"date" : "2018-01-05",
"from_shareholder" : "SH-00003",
"share_type" : "Equity",
"from_no" : 401,
"to_no" : 500,
"no_of_shares" : 100,
"rate" : 25,
"company" : "_Test Company",
"asset_account" : "Cash - _TC",
"doctype": "Share Transfer",
"transfer_type": "Purchase",
"date": "2018-01-05",
"from_shareholder": "SH-00003",
"share_type": "Equity",
"from_no": 401,
"to_no": 500,
"no_of_shares": 100,
"rate": 25,
"company": "_Test Company",
"asset_account": "Cash - _TC",
"equity_or_liability_account": "Creditors - _TC"
}
]
@ -91,33 +91,33 @@ class TestShareTransfer(unittest.TestCase):
def test_invalid_share_transfer(self):
doc = frappe.get_doc({
"doctype" : "Share Transfer",
"transfer_type" : "Transfer",
"date" : "2018-01-05",
"from_shareholder" : "SH-00003",
"to_shareholder" : "SH-00002",
"share_type" : "Equity",
"from_no" : 1,
"to_no" : 100,
"no_of_shares" : 100,
"rate" : 15,
"company" : "_Test Company",
"doctype": "Share Transfer",
"transfer_type": "Transfer",
"date": "2018-01-05",
"from_shareholder": "SH-00003",
"to_shareholder": "SH-00002",
"share_type": "Equity",
"from_no": 1,
"to_no": 100,
"no_of_shares": 100,
"rate": 15,
"company": "_Test Company",
"equity_or_liability_account": "Creditors - _TC"
})
self.assertRaises(ShareDontExists, doc.insert)
doc = frappe.get_doc({
"doctype" : "Share Transfer",
"transfer_type" : "Purchase",
"date" : "2018-01-02",
"from_shareholder" : "SH-00001",
"share_type" : "Equity",
"from_no" : 1,
"to_no" : 200,
"no_of_shares" : 200,
"rate" : 15,
"company" : "_Test Company",
"asset_account" : "Cash - _TC",
"doctype": "Share Transfer",
"transfer_type": "Purchase",
"date": "2018-01-02",
"from_shareholder": "SH-00001",
"share_type": "Equity",
"from_no": 1,
"to_no": 200,
"no_of_shares": 200,
"rate": 15,
"company": "_Test Company",
"asset_account": "Cash - _TC",
"equity_or_liability_account": "Creditors - _TC"
})
self.assertRaises(ShareDontExists, doc.insert)

View File

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

View File

@ -163,23 +163,35 @@ def validate_account_for_perpetual_inventory(gl_map):
.format(account), StockAccountInvalidTransaction)
elif account_bal != stock_bal:
error_reason = _("Account Balance ({0}) and Stock Value ({1}) is out of sync for account {2} and it's linked warehouses.").format(
account_bal, stock_bal, frappe.bold(account))
error_resolution = _("Please create adjustment Journal Entry for amount {0} ").format(frappe.bold(stock_bal - account_bal))
button_text = _("Make Adjustment Entry")
precision = get_field_precision(frappe.get_meta("GL Entry").get_field("debit"),
currency=frappe.get_cached_value('Company', gl_map[0].company, "default_currency"))
frappe.throw("""{0}<br></br>{1}<br></br>
<div style="text-align:right;">
<button class="btn btn-primary" onclick="frappe.new_doc('Journal Entry')">{2}</button>
</div>""".format(error_reason, error_resolution, button_text),
StockValueAndAccountBalanceOutOfSync, title=_('Account Balance Out Of Sync'))
diff = flt(stock_bal - account_bal, precision)
error_reason = _("Stock Value ({0}) and Account Balance ({1}) are out of sync for account {2} and it's linked warehouses.").format(
stock_bal, account_bal, frappe.bold(account))
error_resolution = _("Please create adjustment Journal Entry for amount {0} ").format(frappe.bold(diff))
stock_adjustment_account = frappe.db.get_value("Company",gl_map[0].company,"stock_adjustment_account")
db_or_cr_warehouse_account =('credit_in_account_currency' if diff < 0 else 'debit_in_account_currency')
db_or_cr_stock_adjustment_account = ('debit_in_account_currency' if diff < 0 else 'credit_in_account_currency')
journal_entry_args = {
'accounts':[
{'account': account, db_or_cr_warehouse_account : abs(diff)},
{'account': stock_adjustment_account, db_or_cr_stock_adjustment_account : abs(diff) }]
}
frappe.msgprint(msg="""{0}<br></br>{1}<br></br>""".format(error_reason, error_resolution),
raise_exception=StockValueAndAccountBalanceOutOfSync,
title=_('Values Out Of Sync'),
primary_action={
'label': _('Make Journal Entry'),
'client_action': 'erpnext.route_to_adjustment_jv',
'args': journal_entry_args
})
def validate_cwip_accounts(gl_map):
cwip_enabled = cint(frappe.get_cached_value("Company",
gl_map[0].company, "enable_cwip_accounting"))
if not cwip_enabled:
cwip_enabled = any([cint(ac.enable_cwip_accounting) for ac in frappe.db.get_all("Asset Category","enable_cwip_accounting")])
cwip_enabled = any([cint(ac.enable_cwip_accounting) for ac in frappe.db.get_all("Asset Category","enable_cwip_accounting")])
if cwip_enabled and gl_map[0].voucher_type == "Journal Entry":
cwip_accounts = [d[0] for d in frappe.db.sql("""select name from tabAccount

View File

@ -630,7 +630,7 @@ def get_held_invoices(party_type, party):
'select name from `tabPurchase Invoice` where release_date IS NOT NULL and release_date > CURDATE()',
as_dict=1
)
held_invoices = [d['name'] for d in held_invoices]
held_invoices = set([d['name'] for d in held_invoices])
return held_invoices
@ -639,14 +639,19 @@ def get_outstanding_invoices(party_type, party, account, condition=None, filters
outstanding_invoices = []
precision = frappe.get_precision("Sales Invoice", "outstanding_amount") or 2
if erpnext.get_party_account_type(party_type) == 'Receivable':
if account:
root_type = frappe.get_cached_value("Account", account, "root_type")
party_account_type = "Receivable" if root_type == "Asset" else "Payable"
else:
party_account_type = erpnext.get_party_account_type(party_type)
if party_account_type == 'Receivable':
dr_or_cr = "debit_in_account_currency - credit_in_account_currency"
payment_dr_or_cr = "credit_in_account_currency - debit_in_account_currency"
else:
dr_or_cr = "credit_in_account_currency - debit_in_account_currency"
payment_dr_or_cr = "debit_in_account_currency - credit_in_account_currency"
invoice = 'Sales Invoice' if erpnext.get_party_account_type(party_type) == 'Receivable' else 'Purchase Invoice'
held_invoices = get_held_invoices(party_type, party)
invoice_list = frappe.db.sql("""
@ -665,7 +670,6 @@ def get_outstanding_invoices(party_type, party, account, condition=None, filters
group by voucher_type, voucher_no
order by posting_date, name""".format(
dr_or_cr=dr_or_cr,
invoice = invoice,
condition=condition or ""
), {
"party_type": party_type,

View File

@ -42,6 +42,24 @@ frappe.ui.form.on('Asset', {
},
setup: function(frm) {
frm.make_methods = {
'Asset Movement': () => {
frappe.call({
method: "erpnext.assets.doctype.asset.asset.make_asset_movement",
freeze: true,
args:{
"assets": [{ name: cur_frm.doc.name }]
},
callback: function (r) {
if (r.message) {
var doc = frappe.model.sync(r.message)[0];
frappe.set_route("Form", doc.doctype, doc.name);
}
}
});
},
}
frm.set_query("purchase_receipt", (doc) => {
return {
query: "erpnext.controllers.queries.get_purchase_receipts",
@ -487,92 +505,19 @@ erpnext.asset.restore_asset = function(frm) {
})
};
erpnext.asset.transfer_asset = function(frm) {
var dialog = new frappe.ui.Dialog({
title: __("Transfer Asset"),
fields: [
{
"label": __("Target Location"),
"fieldname": "target_location",
"fieldtype": "Link",
"options": "Location",
"get_query": function () {
return {
filters: [
["Location", "is_group", "=", 0]
]
}
},
"reqd": 1
},
{
"label": __("Select Serial No"),
"fieldname": "serial_nos",
"fieldtype": "Link",
"options": "Serial No",
"get_query": function () {
return {
filters: {
'asset': frm.doc.name
}
}
},
"onchange": function() {
let val = this.get_value();
if (val) {
let serial_nos = dialog.get_value("serial_no") || val;
if (serial_nos) {
serial_nos = serial_nos.split('\n');
serial_nos.push(val);
const unique_sn = serial_nos.filter(function(elem, index, self) {
return index === self.indexOf(elem);
});
dialog.set_value("serial_no", unique_sn.join('\n'));
dialog.set_value("serial_nos", "");
}
}
}
},
{
"label": __("Serial No"),
"fieldname": "serial_no",
"read_only": 1,
"fieldtype": "Small Text"
},
{
"label": __("Date"),
"fieldname": "transfer_date",
"fieldtype": "Datetime",
"reqd": 1,
"default": frappe.datetime.now_datetime()
erpnext.asset.transfer_asset = function() {
frappe.call({
method: "erpnext.assets.doctype.asset.asset.make_asset_movement",
freeze: true,
args:{
"assets": [{ name: cur_frm.doc.name }],
"purpose": "Transfer"
},
callback: function (r) {
if (r.message) {
var doc = frappe.model.sync(r.message)[0];
frappe.set_route("Form", doc.doctype, doc.name);
}
]
}
});
dialog.set_primary_action(__("Transfer"), function() {
var args = dialog.get_values();
if(!args) return;
dialog.hide();
return frappe.call({
type: "GET",
method: "erpnext.assets.doctype.asset.asset.transfer_asset",
args: {
args: {
"asset": frm.doc.name,
"transaction_date": args.transfer_date,
"source_location": frm.doc.location,
"target_location": args.target_location,
"serial_no": args.serial_no,
"company": frm.doc.company
}
},
freeze: true,
callback: function(r) {
cur_frm.reload_doc();
}
})
});
dialog.show();
};

View File

@ -31,10 +31,9 @@ class Asset(AccountsController):
self.validate_in_use_date()
self.set_status()
self.make_asset_movement()
if not self.booked_fixed_asset and is_cwip_accounting_enabled(self.company,
self.asset_category):
if not self.booked_fixed_asset and is_cwip_accounting_enabled(self.asset_category):
self.make_gl_entries()
def before_cancel(self):
self.cancel_auto_gen_movement()
@ -44,7 +43,7 @@ class Asset(AccountsController):
self.set_status()
delete_gl_entries(voucher_type='Asset', voucher_no=self.name)
self.db_set('booked_fixed_asset', 0)
def validate_asset_and_reference(self):
if self.purchase_invoice or self.purchase_receipt:
reference_doc = 'Purchase Invoice' if self.purchase_invoice else 'Purchase Receipt'
@ -52,8 +51,8 @@ class Asset(AccountsController):
reference_doc = frappe.get_doc(reference_doc, reference_name)
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')))
if self.is_existing_asset and self.purchase_invoice:
frappe.throw(_("Purchase Invoice cannot be made against an existing asset {0}").format(self.name))
@ -99,7 +98,7 @@ class Asset(AccountsController):
if not flt(self.gross_purchase_amount):
frappe.throw(_("Gross Purchase Amount is mandatory"), frappe.MandatoryError)
if is_cwip_accounting_enabled(self.company, self.asset_category):
if is_cwip_accounting_enabled(self.asset_category):
if not self.is_existing_asset and not (self.purchase_receipt or self.purchase_invoice):
frappe.throw(_("Please create purchase receipt or purchase invoice for the item {0}").
format(self.item_code))
@ -126,15 +125,17 @@ class Asset(AccountsController):
frappe.throw(_("Available-for-use Date should be after purchase date"))
def cancel_auto_gen_movement(self):
reference_docname = self.purchase_invoice or self.purchase_receipt
movement = frappe.db.get_all('Asset Movement', filters={ 'reference_name': reference_docname, 'docstatus': 1 })
if len(movement) > 1:
movements = frappe.db.sql(
"""SELECT asm.name, asm.docstatus
FROM `tabAsset Movement` asm, `tabAsset Movement Item` asm_item
WHERE asm_item.parent=asm.name and asm_item.asset=%s and asm.docstatus=1""", self.name, as_dict=1)
if len(movements) > 1:
frappe.throw(_('Asset has multiple Asset Movement Entries which has to be \
cancelled manually to cancel this asset.'))
movement = frappe.get_doc('Asset Movement', movement[0].get('name'))
movement = frappe.get_doc('Asset Movement', movements[0].get('name'))
movement.flags.ignore_validate = True
movement.cancel()
def make_asset_movement(self):
reference_doctype = 'Purchase Receipt' if self.purchase_receipt else 'Purchase Invoice'
reference_docname = self.purchase_receipt or self.purchase_invoice
@ -203,7 +204,7 @@ class Asset(AccountsController):
if has_pro_rata and n==0:
depreciation_amount, days, months = get_pro_rata_amt(d, depreciation_amount,
self.available_for_use_date, d.depreciation_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
monthly_schedule_date = add_months(d.depreciation_start_date, - months + 1)
@ -261,7 +262,7 @@ class Asset(AccountsController):
else:
date = add_months(monthly_schedule_date, r)
amount = depreciation_amount / month_range
self.append("schedules", {
"schedule_date": date,
"depreciation_amount": amount,
@ -295,7 +296,9 @@ class Asset(AccountsController):
.format(row.idx))
if not row.depreciation_start_date:
frappe.throw(_("Row {0}: Depreciation Start Date is required").format(row.idx))
if not self.available_for_use_date:
frappe.throw(_("Row {0}: Depreciation Start Date is required").format(row.idx))
row.depreciation_start_date = self.available_for_use_date
if not self.is_existing_asset:
self.opening_accumulated_depreciation = 0
@ -514,7 +517,7 @@ def update_maintenance_status():
asset.set_status('Out of Order')
def make_post_gl_entry():
if not is_cwip_accounting_enabled(self.company, self.asset_category):
if not is_cwip_accounting_enabled(self.asset_category):
return
assets = frappe.db.sql_list(""" select name from `tabAsset`
@ -646,34 +649,21 @@ def make_journal_entry(asset_name):
return je
@frappe.whitelist()
def make_asset_movement(assets):
def make_asset_movement(assets, purpose=None):
import json
from six import string_types
if isinstance(assets, string_types):
assets = json.loads(assets)
if len(assets) == 0:
frappe.throw(_('Atleast one asset has to be selected.'))
asset_movement = frappe.new_doc("Asset Movement")
asset_movement.quantity = len(assets)
prev_reference_docname = ''
for asset in assets:
asset = frappe.get_doc('Asset', asset.get('name'))
# get PR/PI linked with asset
reference_docname = asset.get('purchase_receipt') if asset.get('purchase_receipt') \
else asset.get('purchase_invoice')
# checks if all the assets are linked with a single PR/PI
if prev_reference_docname == '':
prev_reference_docname = reference_docname
elif prev_reference_docname != reference_docname:
frappe.throw(_('Assets selected should belong to same reference document.'))
asset_movement.company = asset.get('company')
asset_movement.reference_doctype = 'Purchase Receipt' if asset.get('purchase_receipt') else 'Purchase Invoice'
asset_movement.reference_name = prev_reference_docname
asset_movement.append("assets", {
'asset': asset.get('name'),
'source_location': asset.get('location'),
@ -683,12 +673,7 @@ def make_asset_movement(assets):
if asset_movement.get('assets'):
return asset_movement.as_dict()
def is_cwip_accounting_enabled(company, asset_category=None):
enable_cwip_in_company = cint(frappe.db.get_value("Company", company, "enable_cwip_accounting"))
if enable_cwip_in_company or not asset_category:
return enable_cwip_in_company
def is_cwip_accounting_enabled(asset_category):
return cint(frappe.db.get_value("Asset Category", asset_category, "enable_cwip_accounting"))
def get_pro_rata_amt(row, depreciation_amount, from_date, to_date):

View File

@ -37,6 +37,7 @@ frappe.listview_settings['Asset'] = {
const assets = me.get_checked_items();
frappe.call({
method: "erpnext.assets.doctype.asset.asset.make_asset_movement",
freeze: true,
args:{
"assets": assets
},

File diff suppressed because it is too large Load Diff

View File

@ -11,7 +11,6 @@ from frappe.model.document import Document
class AssetCategory(Document):
def validate(self):
self.validate_finance_books()
self.validate_enable_cwip_accounting()
def validate_finance_books(self):
for d in self.finance_books:
@ -19,15 +18,6 @@ class AssetCategory(Document):
if cint(d.get(frappe.scrub(field)))<1:
frappe.throw(_("Row {0}: {1} must be greater than 0").format(d.idx, field), frappe.MandatoryError)
def validate_enable_cwip_accounting(self):
if self.enable_cwip_accounting :
for d in self.accounts:
cwip = frappe.db.get_value("Company",d.company_name,"enable_cwip_accounting")
if cwip:
frappe.throw(_
("CWIP is enabled globally in Company {1}. To enable it in Asset Category, first disable it in {1} ").format(
frappe.bold(d.idx), frappe.bold(d.company_name)))
@frappe.whitelist()
def get_asset_category_account(fieldname, item=None, asset=None, account=None, asset_category = None, company = None):
if item and frappe.db.get_value("Item", item, "is_fixed_asset"):

View File

@ -31,6 +31,13 @@ frappe.ui.form.on('Asset Movement', {
name: ["in", ["Purchase Receipt", "Purchase Invoice"]]
}
};
}),
frm.set_query("asset", "assets", () => {
return {
filters: {
status: ["not in", ["Draft"]]
}
}
})
},
@ -76,50 +83,6 @@ frappe.ui.form.on('Asset Movement', {
});
});
frm.refresh_field('assets');
},
reference_name: function(frm) {
if (frm.doc.reference_name && frm.doc.reference_doctype) {
const reference_doctype = frm.doc.reference_doctype === 'Purchase Invoice' ? 'purchase_invoice' : 'purchase_receipt';
// On selection of reference name,
// sets query to display assets linked to that reference doc
frm.set_query('asset', 'assets', function() {
return {
filters: {
[reference_doctype] : frm.doc.reference_name
}
};
});
// fetches linked asset & adds to the assets table
frappe.db.get_list('Asset', {
fields: ['name', 'location', 'custodian'],
filters: {
[reference_doctype] : frm.doc.reference_name
}
}).then((docs) => {
if (docs.length == 0) {
frappe.msgprint(frappe._(`Please select ${frm.doc.reference_doctype} which has assets.`));
frm.doc.reference_name = '';
frm.refresh_field('reference_name');
return;
}
frm.doc.assets = [];
docs.forEach(doc => {
frm.add_child('assets', {
asset: doc.name,
source_location: doc.location,
from_employee: doc.custodian
});
frm.refresh_field('assets');
})
}).catch((err) => {
console.log(err); // eslint-disable-line
});
} else {
// if reference is deleted then remove query
frm.set_query('asset', 'assets', () => ({ filters: {} }));
}
}
});
@ -132,7 +95,7 @@ frappe.ui.form.on('Asset Movement Item', {
if(asset_doc.location) frappe.model.set_value(cdt, cdn, 'source_location', asset_doc.location);
if(asset_doc.custodian) frappe.model.set_value(cdt, cdn, 'from_employee', asset_doc.custodian);
}).catch((err) => {
console.log(err);
console.log(err); // eslint-disable-line
});
}
}

View File

@ -9,12 +9,12 @@
"purpose",
"column_break_4",
"transaction_date",
"section_break_10",
"assets",
"reference",
"reference_doctype",
"column_break_9",
"reference_name",
"section_break_10",
"assets",
"amended_from"
],
"fields": [
@ -47,6 +47,7 @@
"fieldtype": "Column Break"
},
{
"collapsible": 1,
"fieldname": "reference",
"fieldtype": "Section Break",
"label": "Reference"
@ -54,18 +55,16 @@
{
"fieldname": "reference_doctype",
"fieldtype": "Link",
"label": "Reference DocType",
"label": "Reference Document Type",
"no_copy": 1,
"options": "DocType",
"reqd": 1
"options": "DocType"
},
{
"fieldname": "reference_name",
"fieldtype": "Dynamic Link",
"label": "Reference Name",
"label": "Reference Document Name",
"no_copy": 1,
"options": "reference_doctype",
"reqd": 1
"options": "reference_doctype"
},
{
"fieldname": "amended_from",
@ -93,7 +92,7 @@
}
],
"is_submittable": 1,
"modified": "2019-11-13 15:37:48.870147",
"modified": "2019-11-23 13:28:47.256935",
"modified_by": "Administrator",
"module": "Assets",
"name": "Asset Movement",

View File

@ -22,7 +22,7 @@ class AssetMovement(Document):
if company != self.company:
frappe.throw(_("Asset {0} does not belong to company {1}").format(d.asset, self.company))
if not(d.source_location or d.target_location or d.from_employee or d.to_employee):
if not (d.source_location or d.target_location or d.from_employee or d.to_employee):
frappe.throw(_("Either location or employee must be required"))
def validate_location(self):

View File

@ -60,7 +60,8 @@
{
"fieldname": "date",
"fieldtype": "Date",
"label": "Date"
"label": "Date",
"reqd": 1
},
{
"fieldname": "current_asset_value",
@ -110,7 +111,7 @@
}
],
"is_submittable": 1,
"modified": "2019-05-26 09:46:23.613412",
"modified": "2019-11-22 14:09:25.800375",
"modified_by": "Administrator",
"module": "Assets",
"name": "Asset Value Adjustment",

View File

@ -5,12 +5,13 @@
from __future__ import unicode_literals
import frappe
from frappe import _
from frappe.utils import flt, getdate, cint, date_diff
from frappe.utils import flt, getdate, cint, date_diff, formatdate
from erpnext.assets.doctype.asset.depreciation import get_depreciation_accounts
from frappe.model.document import Document
class AssetValueAdjustment(Document):
def validate(self):
self.validate_date()
self.set_difference_amount()
self.set_current_asset_value()
@ -23,6 +24,12 @@ class AssetValueAdjustment(Document):
frappe.throw(_("Cancel the journal entry {0} first").format(self.journal_entry))
self.reschedule_depreciations(self.current_asset_value)
def validate_date(self):
asset_purchase_date = frappe.db.get_value('Asset', self.asset, 'purchase_date')
if getdate(self.date) < getdate(asset_purchase_date):
frappe.throw(_("Asset Value Adjustment cannot be posted before Asset's purchase date <b>{0}</b>.")
.format(formatdate(asset_purchase_date)), title="Incorrect Date")
def set_difference_amount(self):
self.difference_amount = flt(self.current_asset_value - self.new_asset_value)

View File

@ -313,7 +313,7 @@ def item_last_purchase_rate(name, conversion_rate, item_code, conversion_factor=
last_purchase_details = get_last_purchase_details(item_code, name)
if last_purchase_details:
last_purchase_rate = (last_purchase_details['base_rate'] * (flt(conversion_factor) or 1.0)) / conversion_rate
last_purchase_rate = (last_purchase_details['base_net_rate'] * (flt(conversion_factor) or 1.0)) / conversion_rate
return last_purchase_rate
else:
item_last_purchase_rate = frappe.get_cached_value("Item", item_code, "last_purchase_rate")

View File

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

View File

@ -24,12 +24,12 @@ def update_last_purchase_rate(doc, is_submit):
last_purchase_rate = None
if last_purchase_details and \
(last_purchase_details.purchase_date > this_purchase_date):
last_purchase_rate = last_purchase_details['base_rate']
last_purchase_rate = last_purchase_details['base_net_rate']
elif is_submit == 1:
# even if this transaction is the latest one, it should be submitted
# for it to be considered for latest purchase rate
if flt(d.conversion_factor):
last_purchase_rate = flt(d.base_rate) / flt(d.conversion_factor)
last_purchase_rate = flt(d.base_net_rate) / flt(d.conversion_factor)
# Check if item code is present
# Conversion factor should not be mandatory for non itemized items
elif d.item_code:

View File

@ -0,0 +1,14 @@
# Version 12.2.0 Release Notes
### Accounting
1. Fixed Asset
- "Enable CWIP" options moved to Asset Category from Asset Settings
- Removed Asset link from Purchase Receipt Item table
- Enhanced Asset master
- Asset Movement now handles movement of multiple assets
- Introduced monthly depreciation
2. GL Entries for Landed Cost Voucher now posted directly against individual Charges account
3. Optimization of BOM Update Tool
4. Syncing of Stock and Account balance is enforced, in case of perpetual inventory
5. Rendered email template in Email Campaign

View File

@ -46,6 +46,16 @@ def get_data():
"name": "Contract",
"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",
"name": "SMS Settings",
"description": _("Setup SMS gateway settings")
},
{
"type": "doctype",
"label": _("Email Group"),
"name": "Email Group",
}
]
},

View File

@ -577,6 +577,7 @@ class BuyingController(StockController):
def auto_make_assets(self, asset_items):
items_data = get_asset_item_details(asset_items)
messages = []
for d in self.items:
if d.is_fixed_asset:
@ -589,12 +590,16 @@ class BuyingController(StockController):
for qty in range(cint(d.qty)):
self.make_asset(d)
is_plural = 's' if cint(d.qty) != 1 else ''
frappe.msgprint(_('{0} Asset{2} Created for {1}').format(cint(d.qty), d.item_code, is_plural))
messages.append(_('{0} Asset{2} Created for <b>{1}</b>').format(cint(d.qty), d.item_code, is_plural))
else:
frappe.throw(_("Asset Naming Series is mandatory for the auto creation for item {0}").format(d.item_code))
frappe.throw(_("Row {1}: Asset Naming Series is mandatory for the auto creation for item {0}")
.format(d.item_code, d.idx))
else:
frappe.msgprint(_("Assets not created. You will have to create asset manually."))
messages.append(_("Assets not created for <b>{0}</b>. You will have to create asset manually.")
.format(d.item_code))
for message in messages:
frappe.msgprint(message, title="Success")
def make_asset(self, row):
if not row.asset_location:
@ -636,7 +641,10 @@ class BuyingController(StockController):
asset = frappe.get_doc('Asset', asset.name)
if delete_asset and is_auto_create_enabled:
# need to delete movements to delete assets otherwise throws link exists error
movements = frappe.db.get_all('Asset Movement', filters={ 'reference_name': self.name })
movements = frappe.db.sql(
"""SELECT asm.name
FROM `tabAsset Movement` asm, `tabAsset Movement Item` asm_item
WHERE asm_item.parent=asm.name and asm_item.asset=%s""", asset.name, as_dict=1)
for movement in movements:
frappe.delete_doc('Asset Movement', movement.name, force=1)
frappe.delete_doc("Asset", asset.name, force=1)
@ -647,8 +655,12 @@ class BuyingController(StockController):
asset.purchase_date = self.posting_date
asset.supplier = self.supplier
elif self.docstatus == 2:
asset.set(field, None)
asset.supplier = None
if asset.docstatus == 0:
asset.set(field, None)
asset.supplier = None
if asset.docstatus == 1 and delete_asset:
frappe.throw(_('Cannot cancel this document as it is linked with submitted asset {0}.\
Please cancel the it to continue.').format(asset.name))
asset.flags.ignore_validate_update_after_submit = True
asset.flags.ignore_mandatory = True

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

File diff suppressed because it is too large Load Diff

View File

@ -301,7 +301,8 @@ scheduler_events = {
"erpnext.quality_management.doctype.quality_review.quality_review.review",
"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.set_email_campaign_status"
"erpnext.crm.doctype.email_campaign.email_campaign.set_email_campaign_status",
"erpnext.selling.doctype.quotation.set_expired_status"
],
"daily_long": [
"erpnext.setup.doctype.email_digest.email_digest.send",

View File

@ -6,6 +6,7 @@ from __future__ import unicode_literals
import frappe
import json
from frappe.model.document import Document
from frappe.utils import getdate
class EmployeeAttendanceTool(Document):
@ -43,17 +44,26 @@ def get_employees(date, department = None, branch = None, company = None):
@frappe.whitelist()
def mark_employee_attendance(employee_list, status, date, leave_type=None, company=None):
employee_list = json.loads(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:
attendance.leave_type = leave_type
if company:
attendance.company = company
leave_type = leave_type
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()

View File

@ -208,6 +208,24 @@ frappe.ui.form.on("Expense Claim", {
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) {
var method = "erpnext.accounts.doctype.payment_entry.payment_entry.get_payment_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_paid = d.paid_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");
}

View File

@ -140,32 +140,6 @@ class ExpenseClaim(AccountsController):
"against": ",".join([d.default_account for d in self.expenses]),
"party_type": "Employee",
"party": self.employee,
"against_voucher_type": self.doctype,
"against_voucher": self.name
})
)
gl_entry.append(
self.get_gl_dict({
"account": data.advance_account,
"debit": data.allocated_amount,
"debit_in_account_currency": data.allocated_amount,
"against": self.payable_account,
"party_type": "Employee",
"party": self.employee,
"against_voucher_type": self.doctype,
"against_voucher": self.name
})
)
gl_entry.append(
self.get_gl_dict({
"account": self.payable_account,
"credit": data.allocated_amount,
"credit_in_account_currency": data.allocated_amount,
"against": data.advance_account,
"party_type": "Employee",
"party": self.employee,
"against_voucher_type": "Employee Advance",
"against_voucher": data.employee_advance
})

View File

@ -5,6 +5,12 @@ frappe.provide("erpnext.bom");
frappe.ui.form.on("BOM", {
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() {
return {
filters: {
@ -85,9 +91,21 @@ frappe.ui.form.on("BOM", {
}
if(frm.doc.docstatus!=0) {
frm.add_custom_button(__("Duplicate"), function() {
frm.add_custom_button(__("Duplicate BOM"), function() {
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) {
@ -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) {
return frappe.call({
doc: frm.doc,

View File

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

View File

@ -96,6 +96,7 @@ class BOM(WebsiteGenerator):
def get_routing(self):
if self.routing:
self.set("operations", [])
for d in frappe.get_all("BOM Operation", fields = ["*"],
filters = {'parenttype': 'Routing', 'parent': self.routing}):
child = self.append('operations', d)
@ -289,7 +290,7 @@ class BOM(WebsiteGenerator):
if not 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):
""" Uncheck others if current one is selected as default or
@ -362,15 +363,9 @@ class BOM(WebsiteGenerator):
def validate_materials(self):
""" 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'):
frappe.throw(_("Raw Materials cannot be blank."))
check_list = []
for m in self.get('items'):
if m.bom_no:
@ -379,16 +374,6 @@ class BOM(WebsiteGenerator):
frappe.throw(_("Quantity required for Item {0} in row {1}").format(m.item_code, m.idx))
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=[]):
""" Check whether recursion occurs in any bom"""
bom_list = self.traverse_tree()

View File

@ -17,11 +17,13 @@ def get_data():
},
{
'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']
}
]
],
'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

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

View File

@ -609,6 +609,22 @@ def get_item_details(item, project = None):
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 qty > 0:
wo_doc.qty = qty
wo_doc.get_items_and_operations_from_bom()
return wo_doc
@frappe.whitelist()
def check_if_scrap_warehouse_mandatory(bom_no):
res = {"set_scrap_wh_mandatory": False }

View File

@ -645,4 +645,5 @@ erpnext.patches.v12_0.replace_accounting_with_accounts_in_home_settings
erpnext.patches.v12_0.set_payment_entry_status
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.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

View File

@ -15,13 +15,6 @@ def execute():
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']:
frappe.reload_doc('manufacturing', 'doctype', frappe.scrub(doctype))

View File

@ -7,15 +7,11 @@ def execute():
'''Get 'Disable CWIP Accounting value' from Asset Settings, set it in 'Enable Capital Work in Progress Accounting' field
in Company, delete Asset Settings '''
if frappe.db.exists("DocType","Asset Settings"):
frappe.reload_doctype("Company")
cwip_value = frappe.db.get_single_value("Asset Settings","disable_cwip_accounting")
if frappe.db.exists("DocType", "Asset Settings"):
frappe.reload_doctype("Asset Category")
cwip_value = frappe.db.get_single_value("Asset Settings", "disable_cwip_accounting")
frappe.db.sql("""UPDATE `tabAsset Category` SET enable_cwip_accounting = %s""", cint(cwip_value))
companies = [x['name'] for x in frappe.get_all("Company", "name")]
for company in companies:
enable_cwip_accounting = cint(not cint(cwip_value))
frappe.db.set_value("Company", company, "enable_cwip_accounting", enable_cwip_accounting)
frappe.db.sql(
""" DELETE FROM `tabSingles` where doctype = 'Asset Settings' """)
frappe.delete_doc_if_exists("DocType","Asset Settings")
frappe.db.sql("""DELETE FROM `tabSingles` where doctype = 'Asset Settings'""")
frappe.delete_doc_if_exists("DocType", "Asset Settings")

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

@ -7,7 +7,7 @@ import json
import frappe
from frappe import _, throw
from frappe.utils import add_days, cstr, date_diff, get_link_to_form, getdate
from frappe.utils import add_days, cstr, date_diff, get_link_to_form, getdate, today
from frappe.utils.nestedset import NestedSet
from frappe.desk.form.assign_to import close_all_assignments, clear
from frappe.utils import date_diff
@ -212,8 +212,11 @@ def set_multiple_status(names, status):
task.save()
def set_tasks_as_overdue():
tasks = frappe.get_all("Task", filters={'status':['not in',['Cancelled', 'Completed']]})
tasks = frappe.get_all("Task", filters={'status':['not in',['Cancelled', 'Closed']]})
for task in tasks:
if frappe.db.get_value("Task", task.name, "status") in 'Pending Review':
if getdate(frappe.db.get_value("Task", task.name, "review_date")) < getdate(today()):
continue
frappe.get_doc("Task", task.name).update_status()
@frappe.whitelist()

View File

@ -64,7 +64,7 @@ frappe.ui.form.on(cur_frm.doctype, {
}
})
}
}
}
});
frappe.ui.form.on('Sales Invoice Payment', {
@ -355,4 +355,4 @@ cur_frm.pformat.taxes= function(doc){
out += '</table></td></tr></table></div>';
}
return out;
}
}

View File

@ -74,6 +74,22 @@ $.extend(erpnext, {
);
});
},
route_to_adjustment_jv: (args) => {
frappe.model.with_doctype('Journal Entry', () => {
// route to adjustment Journal Entry to handle Account Balance and Stock Value mismatch
let journal_entry = frappe.model.get_new_doc('Journal Entry');
args.accounts.forEach((je_account) => {
let child_row = frappe.model.add_child(journal_entry, "accounts");
child_row.account = je_account.account;
child_row.debit_in_account_currency = je_account.debit_in_account_currency;
child_row.credit_in_account_currency = je_account.credit_in_account_currency;
child_row.party_type = "" ;
});
frappe.set_route('Form','Journal Entry', journal_entry.name);
});
}
});

View File

@ -205,7 +205,9 @@
{%- endif %}
<ImponibileImporto>{{ format_float(data.taxable_amount, item_meta.get_field("tax_amount").precision) }}</ImponibileImporto>
<Imposta>{{ format_float(data.tax_amount, item_meta.get_field("tax_amount").precision) }}</Imposta>
<EsigibilitaIVA>{{ doc.vat_collectability.split("-")[0] }}</EsigibilitaIVA>
{%- if data.vat_collectability %}
<EsigibilitaIVA>{{ doc.vat_collectability.split("-")[0] }}</EsigibilitaIVA>
{%- endif %}
{%- if data.tax_exemption_law %}
<RiferimentoNormativo>{{ data.tax_exemption_law }}</RiferimentoNormativo>
{%- endif %}

View File

@ -49,9 +49,9 @@ frappe.ui.form.on("Customer", {
})
frm.set_query('customer_primary_address', function(doc) {
return {
query: "erpnext.selling.doctype.customer.customer.get_customer_primary_address",
filters: {
'customer': doc.name
'link_doctype': 'Customer',
'link_name': doc.name
}
}
})

View File

@ -397,15 +397,3 @@ def get_customer_primary_contact(doctype, txt, searchfield, start, page_len, fil
'customer': customer,
'txt': '%%%s%%' % txt
})
def get_customer_primary_address(doctype, txt, searchfield, start, page_len, filters):
customer = frappe.db.escape(filters.get('customer'))
return frappe.db.sql("""
select `tabAddress`.name from `tabAddress`, `tabDynamic Link`
where `tabAddress`.name = `tabDynamic Link`.parent and `tabDynamic Link`.link_name = %(customer)s
and `tabDynamic Link`.link_doctype = 'Customer'
and `tabAddress`.name like %(txt)s
""", {
'customer': customer,
'txt': '%%%s%%' % txt
})

File diff suppressed because it is too large Load Diff

View File

@ -185,6 +185,10 @@ def _make_sales_order(source_name, target_doc=None, ignore_permissions=False):
return doclist
def set_expired_status():
frappe.db.sql("""UPDATE `tabQuotation` SET `status` = 'Expired'
WHERE `status` != "Expired" AND `valid_till` < %s""", (nowdate()))
@frappe.whitelist()
def make_sales_invoice(source_name, target_doc=None):
return _make_sales_invoice(source_name, target_doc)

View File

@ -14,15 +14,13 @@ frappe.listview_settings['Quotation'] = {
get_indicator: function(doc) {
if(doc.status==="Open") {
if (doc.valid_till && doc.valid_till < frappe.datetime.nowdate()) {
return [__("Expired"), "darkgrey", "valid_till,<," + frappe.datetime.nowdate()];
} else {
return [__("Open"), "orange", "status,=,Open"];
}
return [__("Open"), "orange", "status,=,Open"];
} else if(doc.status==="Ordered") {
return [__("Ordered"), "green", "status,=,Ordered"];
} else if(doc.status==="Lost") {
return [__("Lost"), "darkgrey", "status,=,Lost"];
} else if(doc.status==="Expired") {
return [__("Expired"), "darkgrey", "status,=,Expired"];
}
}
};

View File

@ -201,6 +201,28 @@ class TestQuotation(unittest.TestCase):
sec_qo = make_quotation(item_list=qo_item2, do_not_submit=True)
sec_qo.submit()
def test_quotation_expiry(self):
from erpnext.selling.doctype.quotation.quotation import set_expired_status
quotation_item = [
{
"item_code": "_Test Item",
"warehouse":"",
"qty": 1,
"rate": 500
}
]
yesterday = add_days(nowdate(), -1)
expired_quotation = make_quotation(item_list=quotation_item, transaction_date=yesterday, do_not_submit=True)
expired_quotation.valid_till = yesterday
expired_quotation.save()
expired_quotation.submit()
set_expired_status()
expired_quotation.reload()
self.assertEqual(expired_quotation.status, "Expired")
test_records = frappe.get_test_records('Quotation')
def get_quotation_dict(party_name=None, item_code=None):
@ -258,3 +280,5 @@ def make_quotation(**args):
qo.submit()
return qo

View File

@ -29,7 +29,7 @@ frappe.ui.form.on("Company", {
company_name: function(frm) {
if(frm.doc.__islocal) {
# add missing " " arg in split method
// add missing " " arg in split method
let parts = frm.doc.company_name.split(" ");
let abbr = $.map(parts, function (p) {
return p? p.substr(0, 1) : null;

View File

@ -72,7 +72,6 @@
"stock_received_but_not_billed",
"expenses_included_in_valuation",
"fixed_asset_depreciation_settings",
"enable_cwip_accounting",
"accumulated_depreciation_account",
"depreciation_expense_account",
"series_for_depreciation_entry",
@ -721,18 +720,12 @@
"fieldtype": "Link",
"label": "Default Buying Terms",
"options": "Terms and Conditions"
},
{
"default": "0",
"fieldname": "enable_cwip_accounting",
"fieldtype": "Check",
"label": "Enable Capital Work in Progress Accounting"
}
],
"icon": "fa fa-building",
"idx": 1,
"image_field": "company_logo",
"modified": "2019-10-09 14:42:04.440974",
"modified": "2019-11-22 13:04:47.470768",
"modified_by": "Administrator",
"module": "Setup",
"name": "Company",

View File

@ -49,7 +49,7 @@ frappe.ui.form.on("Item", {
if (!frm.doc.is_fixed_asset) {
erpnext.item.make_dashboard(frm);
}
if (frm.doc.is_fixed_asset) {
frm.trigger('is_fixed_asset');
frm.trigger('auto_create_assets');
@ -140,6 +140,7 @@ frappe.ui.form.on("Item", {
// set serial no to false & toggles its visibility
frm.set_value('has_serial_no', 0);
frm.toggle_enable(['has_serial_no', 'serial_no_series'], !frm.doc.is_fixed_asset);
frm.toggle_reqd(['asset_category'], frm.doc.is_fixed_asset);
frm.toggle_display(['has_serial_no', 'serial_no_series'], !frm.doc.is_fixed_asset);
frm.call({
@ -150,6 +151,8 @@ frappe.ui.form.on("Item", {
frm.trigger("set_asset_naming_series");
}
});
frm.trigger('auto_create_assets');
},
set_asset_naming_series: function(frm) {
@ -159,8 +162,8 @@ frappe.ui.form.on("Item", {
},
auto_create_assets: function(frm) {
frm.toggle_reqd(['asset_category', 'asset_naming_series'], frm.doc.auto_create_assets);
frm.toggle_display(['asset_category', 'asset_naming_series'], frm.doc.auto_create_assets);
frm.toggle_reqd(['asset_naming_series'], frm.doc.auto_create_assets);
frm.toggle_display(['asset_naming_series'], frm.doc.auto_create_assets);
},
page_name: frappe.utils.warn_page_name_change,

View File

@ -645,7 +645,7 @@ class Item(WebsiteGenerator):
json.dumps(item_wise_tax_detail), update_modified=False)
def set_last_purchase_rate(self, new_name):
last_purchase_rate = get_last_purchase_details(new_name).get("base_rate", 0)
last_purchase_rate = get_last_purchase_details(new_name).get("base_net_rate", 0)
frappe.db.set_value("Item", new_name, "last_purchase_rate", last_purchase_rate)
def recalculate_bin_qty(self, new_name):
@ -942,7 +942,7 @@ def get_last_purchase_details(item_code, doc_name=None, conversion_rate=1.0):
last_purchase_order = frappe.db.sql("""\
select po.name, po.transaction_date, po.conversion_rate,
po_item.conversion_factor, po_item.base_price_list_rate,
po_item.discount_percentage, po_item.base_rate
po_item.discount_percentage, po_item.base_rate, po_item.base_net_rate
from `tabPurchase Order` po, `tabPurchase Order Item` po_item
where po.docstatus = 1 and po_item.item_code = %s and po.name != %s and
po.name = po_item.parent
@ -953,7 +953,7 @@ def get_last_purchase_details(item_code, doc_name=None, conversion_rate=1.0):
last_purchase_receipt = frappe.db.sql("""\
select pr.name, pr.posting_date, pr.posting_time, pr.conversion_rate,
pr_item.conversion_factor, pr_item.base_price_list_rate, pr_item.discount_percentage,
pr_item.base_rate
pr_item.base_rate, pr_item.base_net_rate
from `tabPurchase Receipt` pr, `tabPurchase Receipt Item` pr_item
where pr.docstatus = 1 and pr_item.item_code = %s and pr.name != %s and
pr.name = pr_item.parent
@ -984,6 +984,7 @@ def get_last_purchase_details(item_code, doc_name=None, conversion_rate=1.0):
out = frappe._dict({
"base_price_list_rate": flt(last_purchase.base_price_list_rate) / conversion_factor,
"base_rate": flt(last_purchase.base_rate) / conversion_factor,
"base_net_rate": flt(last_purchase.net_rate) / conversion_factor,
"discount_percentage": flt(last_purchase.discount_percentage),
"purchase_date": purchase_date
})
@ -992,7 +993,8 @@ def get_last_purchase_details(item_code, doc_name=None, conversion_rate=1.0):
out.update({
"price_list_rate": out.base_price_list_rate / conversion_rate,
"rate": out.base_rate / conversion_rate,
"base_rate": out.base_rate
"base_rate": out.base_rate,
"base_net_rate": out.base_net_rate
})
return out

View File

@ -8,11 +8,11 @@
"naming_series",
"company",
"purchase_receipts",
"sec_break1",
"taxes",
"purchase_receipt_items",
"get_items_from_purchase_receipts",
"items",
"sec_break1",
"taxes",
"section_break_9",
"total_taxes_and_charges",
"col_break1",
@ -123,7 +123,7 @@
],
"icon": "icon-usd",
"is_submittable": 1,
"modified": "2019-10-09 13:39:36.082777",
"modified": "2019-11-21 15:34:10.846093",
"modified_by": "Administrator",
"module": "Stock",
"name": "Landed Cost Voucher",

View File

@ -138,8 +138,8 @@ class LandedCostVoucher(Document):
if item.is_fixed_asset:
receipt_document_type = 'purchase_invoice' if item.receipt_document_type == 'Purchase Invoice' \
else 'purchase_receipt'
docs = frappe.db.get_all('Asset', filters={ receipt_document_type: item.receipt_document },
fields=['name', 'docstatus'])
docs = frappe.db.get_all('Asset', filters={ receipt_document_type: item.receipt_document,
'item_code': item.item_code }, fields=['name', 'docstatus'])
if not docs or len(docs) != item.qty:
frappe.throw(_('There are not enough asset created or linked to {0}. \
Please create or link {1} Assets with respective document.').format(item.receipt_document, item.qty))
@ -148,8 +148,7 @@ class LandedCostVoucher(Document):
if d.docstatus == 1:
frappe.throw(_('{2} <b>{0}</b> has submitted Assets.\
Remove Item <b>{1}</b> from table to continue.').format(
item.receipt_document, item.item_code, item.receipt_document_type)
)
item.receipt_document, item.item_code, item.receipt_document_type))
def update_rate_in_serial_no_for_non_asset_items(self, receipt_document):
for item in receipt_document.get("items"):

View File

@ -82,11 +82,21 @@ class PurchaseReceipt(BuyingController):
self.validate_with_previous_doc()
self.validate_uom_is_integer("uom", ["qty", "received_qty"])
self.validate_uom_is_integer("stock_uom", "stock_qty")
self.validate_cwip_accounts()
self.check_on_hold_or_closed_status()
if getdate(self.posting_date) > getdate(nowdate()):
throw(_("Posting Date cannot be future date"))
def validate_cwip_accounts(self):
for item in self.get('items'):
if item.is_fixed_asset and is_cwip_accounting_enabled(item.asset_category):
# check cwip accounts before making auto assets
# Improves UX by not giving messages of "Assets Created" before throwing error of not finding arbnb account
arbnb_account = self.get_company_default("asset_received_but_not_billed")
cwip_account = get_asset_account("capital_work_in_progress_account", company = self.company)
break
def validate_with_previous_doc(self):
super(PurchaseReceipt, self).validate_with_previous_doc({
@ -343,7 +353,7 @@ class PurchaseReceipt(BuyingController):
def get_asset_gl_entry(self, gl_entries):
for item in self.get("items"):
if item.is_fixed_asset:
if is_cwip_accounting_enabled(self.company, item.asset_category):
if is_cwip_accounting_enabled(item.asset_category):
self.add_asset_gl_entries(item, gl_entries)
if flt(item.landed_cost_voucher_amount):
self.add_lcv_gl_entries(item, gl_entries)
@ -386,7 +396,7 @@ class PurchaseReceipt(BuyingController):
def add_lcv_gl_entries(self, item, gl_entries):
expenses_included_in_asset_valuation = self.get_company_default("expenses_included_in_asset_valuation")
if not is_cwip_accounting_enabled(self.company, item.asset_category):
if not is_cwip_accounting_enabled(item.asset_category):
asset_account = get_asset_category_account(asset_category=item.asset_category, \
fieldname='fixed_asset_account', company=self.company)
else:

View File

@ -6,6 +6,7 @@ import frappe
from frappe.model.document import Document
from erpnext.stock.doctype.quality_inspection_template.quality_inspection_template \
import get_template_details
from frappe.model.mapper import get_mapped_doc
class QualityInspection(Document):
def validate(self):
@ -84,3 +85,37 @@ def item_query(doctype, txt, searchfield, start, page_len, filters):
parent=filters.get('parent'), cond = cond, mcond = mcond, start = start,
page_len = page_len, qi_condition = qi_condition),
{'parent': filters.get('parent'), 'txt': "%%%s%%" % txt})
def quality_inspection_query(doctype, txt, searchfield, start, page_len, filters):
return frappe.get_all('Quality Inspection',
limit_start=start,
limit_page_length=page_len,
filters = {
'docstatus': 1,
'name': ('like', '%%%s%%' % txt),
'item_code': filters.get("item_code"),
'reference_name': ('in', [filters.get("reference_name", ''), ''])
}, as_list=1)
@frappe.whitelist()
def make_quality_inspection(source_name, target_doc=None):
def postprocess(source, doc):
doc.inspected_by = frappe.session.user
doc.get_quality_inspection_template()
doc = get_mapped_doc("BOM", source_name, {
'BOM': {
"doctype": "Quality Inspection",
"validation": {
"docstatus": ["=", 1]
},
"field_map": {
"name": "bom_no",
"item": "item_code",
"stock_uom": "uom",
"stock_qty": "qty"
},
}
}, target_doc, postprocess)
return doc

View File

@ -102,11 +102,12 @@ frappe.ui.form.on('Stock Entry', {
frm.set_query("quality_inspection", "items", function(doc, cdt, cdn) {
var d = locals[cdt][cdn];
return {
query:"erpnext.stock.doctype.quality_inspection.quality_inspection.quality_inspection_query",
filters: {
docstatus: 1,
item_code: d.item_code,
reference_name: doc.name
'item_code': d.item_code,
'reference_name': doc.name
}
}
});

View File

@ -91,6 +91,7 @@ class StockEntry(StockController):
self.update_cost_in_project()
self.validate_reserved_serial_no_consumption()
self.update_transferred_qty()
self.update_quality_inspection()
if self.work_order and self.purpose == "Manufacture":
self.update_so_in_serial_number()
@ -108,6 +109,7 @@ class StockEntry(StockController):
self.make_gl_entries_on_cancel()
self.update_cost_in_project()
self.update_transferred_qty()
self.update_quality_inspection()
def set_job_card_data(self):
if self.job_card and not self.work_order:
@ -1285,6 +1287,20 @@ class StockEntry(StockController):
self._update_percent_field_in_targets(args, update_modified=True)
def update_quality_inspection(self):
if self.inspection_required:
reference_type = reference_name = ''
if self.docstatus == 1:
reference_name = self.name
reference_type = 'Stock Entry'
for d in self.items:
if d.quality_inspection:
frappe.db.set_value("Quality Inspection", d.quality_inspection, {
'reference_type': reference_type,
'reference_name': reference_name
})
@frappe.whitelist()
def move_sample_to_retention_warehouse(company, items):
if isinstance(items, string_types):

View File

@ -292,7 +292,7 @@ def validate_filters(filters):
if not (filters.get("item_code") or filters.get("warehouse")):
sle_count = flt(frappe.db.sql("""select count(name) from `tabStock Ledger Entry`""")[0][0])
if sle_count > 500000:
frappe.throw(_("Please set filter based on Item or Warehouse"))
frappe.throw(_("Please set filter based on Item or Warehouse due to a large amount of entries."))
def get_variants_attributes():
'''Return all item variant attributes.'''

View File

@ -0,0 +1,10 @@
<p>{{_("Dear")}} {{ full_name }}{% if last_name %} {{ last_name}}{% endif %},</p>
<p>{{_("A new appointment has been created for you with {0}").format(site_url)}}.</p>
<p>{{_("Click on the link below to verify your email and confirm the appointment")}}.</p>
<p style="margin: 30px 0px;">
<a href="{{ link }}" rel="nofollow" style="padding: 8px 20px; background-color: #7575ff; color: #fff; border-radius: 4px; text-decoration: none; line-height: 1; border-bottom: 3px solid rgba(0, 0, 0, 0.2); font-size: 14px; font-weight: 200;">{{ _("Verify Email") }}</a>
</p>
<br>
<p style="font-size: 85%;">{{_("You can also copy-paste this link in your browser")}} <a href="{{ link }}">{{ link }}</a></p>

View File

@ -0,0 +1,53 @@
.time-slot {
margin-bottom: 2em;
margin-left: 0.5em;
margin-right: 0.5em;
border-radius: 0.4em;
cursor: pointer;
border: 0.5px solid #cccccc;
min-height: 75px;
padding: 0.5em 1em;
}
@media (max-width: 768px) {
#submit-button-area {
display: grid;
grid-template-areas:
"submit"
"back";
}
}
#customer-form{
border-color: black;
}
#customer-form ::placeholder{
color: #ddd;
}
#timeslot-container{
display: flex;
flex-wrap: wrap;
justify-content: center;
}
.time-slot:hover {
background: #ddd;
}
.time-slot.unavailable {
background: #CBD5E0;
cursor: not-allowed;
color: #718096
}
.time-slot.unavailable .text-muted {
color: #718096
}
.time-slot.selected {
color: white;
background: #5e64ff;
}
.time-slot.selected .text-muted {
color: #EDF2F7 !important;
}

View File

@ -0,0 +1,66 @@
{% extends "templates/web.html" %}
{% block title %}{{ _("Book Appointment") }}{% endblock %}
{% block script %}
<script src="assets/js/moment-bundle.min.js"></script>
<script src="book-appointment/index.js"></script>
{% endblock %}
{% block page_content %}
<div class="container">
<!-- title: Book an appointment -->
<div id="select-date-time">
<div class="text-center mt-5">
<h3>Book an appointment</h3>
<p class="lead text-muted" id="lead-text">Select the date and your timezone</p>
</div>
<div class="row justify-content-center mt-3">
<div class="col-md-6 align-self-center ">
<div class="row">
<input type="date" oninput="on_date_or_timezone_select()" name="appointment-date"
id="appointment-date" class="form-control mt-3 col-md m-3">
<select name="appointment-timezone" oninput="on_date_or_timezone_select()" id="appointment-timezone"
class="form-control m-3 col-md">
</select>
</div>
</div>
</div>
<div class="mt-3" id="timeslot-container">
</div>
<div class="row justify-content-center mt-3">
<div class="col-md-4 mb-3">
<button class="btn btn-primary form-control" id="next-button">Next</button>
</div>
</div>
</div>
</div>
<!--Enter Details-->
<div id="enter-details" class="mb-5">
<div class="text-center mt-5">
<h3>Add details</h3>
<p class="lead text-muted">Selected date is <span class="date-span"></span> at <span class="time-span">
</span></p>
</div>
<div class="row justify-content-center mt-3">
<div class="col-md-4 align-items-center">
<form id="customer-form" action='#'>
<input class="form-control mt-3" type="text" name="customer_name" id="customer_name" placeholder="Your Name (required)" required>
<input class="form-control mt-3" type="tel" name="customer_number" id="customer_number" placeholder="+910000000000">
<input class="form-control mt-3" type="text" name="customer_skype" id="customer_skype" placeholder="Skype">
<input class="form-control mt-3"type="email" name="customer_email" id="customer_email" placeholder="Email Address (required)" required>
<textarea class="form-control mt-3" name="customer_notes" id="customer_notes" cols="30" rows="10"
placeholder="Notes"></textarea>
</form>
<div class="row mt-3 " id="submit-button-area">
<div class="col-md mt-3" style="grid-area: back;"><button class="btn btn-dark form-control" onclick="initialise_select_date()">Go back</button></div>
<div class="col-md mt-3" style="grid-area: submit;"><button class="btn btn-primary form-control " onclick="submit()" id="submit-button">Submit</button></div>
</div>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,236 @@
frappe.ready(async () => {
initialise_select_date();
})
window.holiday_list = [];
async function initialise_select_date() {
navigate_to_page(1);
await get_global_variables();
setup_date_picker();
setup_timezone_selector();
hide_next_button();
}
async function get_global_variables() {
// Using await through this file instead of then.
window.appointment_settings = (await frappe.call({
method: 'erpnext.www.book-appointment.index.get_appointment_settings'
})).message;
window.timezones = (await frappe.call({
method:'erpnext.www.book-appointment.index.get_timezones'
})).message;
window.holiday_list = window.appointment_settings.holiday_list;
}
function setup_timezone_selector() {
/**
* window.timezones is a dictionary with the following structure
* { IANA name: Pretty name}
* For example : { Asia/Kolkata : "India Time - Asia/Kolkata"}
*/
let timezones_element = document.getElementById('appointment-timezone');
let offset = new Date().getTimezoneOffset();
Object.keys(window.timezones).forEach((timezone) => {
let opt = document.createElement('option');
opt.value = timezone;
if (timezone == moment.tz.guess()) {
opt.selected = true;
}
opt.innerHTML = window.timezones[timezone]
timezones_element.appendChild(opt)
});
}
function setup_date_picker() {
let date_picker = document.getElementById('appointment-date');
let today = new Date();
date_picker.min = today.toISOString().substr(0, 10);
today.setDate(today.getDate() + window.appointment_settings.advance_booking_days);
date_picker.max = today.toISOString().substr(0, 10);
}
function hide_next_button() {
let next_button = document.getElementById('next-button');
next_button.disabled = true;
next_button.onclick = () => frappe.msgprint("Please select a date and time");
}
function show_next_button() {
let next_button = document.getElementById('next-button');
next_button.disabled = false;
next_button.onclick = setup_details_page;
}
function on_date_or_timezone_select() {
let date_picker = document.getElementById('appointment-date');
let timezone = document.getElementById('appointment-timezone');
if (date_picker.value === '') {
clear_time_slots();
hide_next_button();
frappe.throw('Please select a date');
}
window.selected_date = date_picker.value;
window.selected_timezone = timezone.value;
update_time_slots(date_picker.value, timezone.value);
let lead_text = document.getElementById('lead-text');
lead_text.innerHTML = "Select Time"
}
async function get_time_slots(date, timezone) {
let slots = (await frappe.call({
method: 'erpnext.www.book-appointment.index.get_appointment_slots',
args: {
date: date,
timezone: timezone
}
})).message;
return slots;
}
async function update_time_slots(selected_date, selected_timezone) {
let timeslot_container = document.getElementById('timeslot-container');
window.slots = await get_time_slots(selected_date, selected_timezone);
clear_time_slots();
if (window.slots.length <= 0) {
let message_div = document.createElement('p');
message_div.innerHTML = "There are no slots available on this date";
timeslot_container.appendChild(message_div);
return
}
window.slots.forEach((slot, index) => {
// Get and append timeslot div
let timeslot_div = get_timeslot_div_layout(slot)
timeslot_container.appendChild(timeslot_div);
});
set_default_timeslot();
}
function get_timeslot_div_layout(timeslot) {
let start_time = new Date(timeslot.time)
let timeslot_div = document.createElement('div');
timeslot_div.classList.add('time-slot');
if (!timeslot.availability) {
timeslot_div.classList.add('unavailable')
}
timeslot_div.innerHTML = get_slot_layout(start_time);
timeslot_div.id = timeslot.time.substr(11, 20);
timeslot_div.addEventListener('click', select_time);
return timeslot_div
}
function clear_time_slots() {
// Clear any existing divs in timeslot container
let timeslot_container = document.getElementById('timeslot-container');
while (timeslot_container.firstChild) {
timeslot_container.removeChild(timeslot_container.firstChild);
}
}
function get_slot_layout(time) {
let timezone = document.getElementById("appointment-timezone").value;
time = new Date(time);
let start_time_string = moment(time).tz(timezone).format("LT");
let end_time = moment(time).tz(timezone).add(window.appointment_settings.appointment_duration, 'minutes');
let end_time_string = end_time.format("LT");
return `<span style="font-size: 1.2em;">${start_time_string}</span><br><span class="text-muted small">to ${end_time_string}</span>`;
}
function select_time() {
if (this.classList.contains('unavailable')) {
return;
}
let selected_element = document.getElementsByClassName('selected');
if (!(selected_element.length > 0)) {
this.classList.add('selected');
show_next_button();
return;
}
selected_element = selected_element[0]
window.selected_time = this.id;
selected_element.classList.remove('selected');
this.classList.add('selected');
show_next_button();
}
function set_default_timeslot() {
let timeslots = document.getElementsByClassName('time-slot')
// Can't use a forEach here since, we need to break the loop after a timeslot is selected
for (let i = 0; i < timeslots.length; i++) {
const timeslot = timeslots[i];
if (!timeslot.classList.contains('unavailable')) {
timeslot.classList.add('selected');
break;
}
}
}
function navigate_to_page(page_number) {
let page1 = document.getElementById('select-date-time');
let page2 = document.getElementById('enter-details');
switch (page_number) {
case 1:
page1.style.display = 'block';
page2.style.display = 'none';
break;
case 2:
page1.style.display = 'none';
page2.style.display = 'block';
break;
default:
break;
}
}
function setup_details_page() {
navigate_to_page(2)
let date_container = document.getElementsByClassName('date-span')[0];
let time_container = document.getElementsByClassName('time-span')[0];
date_container.innerHTML = moment(window.selected_date).format("MMM Do YYYY");
time_container.innerHTML = moment(window.selected_time, "HH:mm:ss").format("LT");
}
async function submit() {
let button = document.getElementById('submit-button');
button.disabled = true;
let form = document.querySelector('#customer-form');
if (!form.checkValidity()) {
form.reportValidity();
button.disabled = false;
return;
}
let contact = get_form_data();
let appointment = frappe.call({
method: 'erpnext.www.book-appointment.index.create_appointment',
args: {
'date': window.selected_date,
'time': window.selected_time,
'contact': contact,
'tz':window.selected_timezone
},
callback: (response)=>{
if (response.message.status == "Unverified") {
frappe.show_alert("Please check your email to confirm the appointment")
} else {
frappe.show_alert("Appointment Created Successfully");
}
setTimeout(()=>{
let redirect_url = "/";
if (window.appointment_settings.success_redirect_url){
redirect_url += window.appointment_settings.success_redirect_url;
}
window.location.href = redirect_url;},5000)
},
error: (err)=>{
frappe.show_alert("Something went wrong please try again");
button.disabled = false;
}
});
}
function get_form_data() {
contact = {};
let inputs = ['name', 'skype', 'number', 'notes', 'email'];
inputs.forEach((id) => contact[id] = document.getElementById(`customer_${id}`).value)
return contact
}

View File

@ -0,0 +1,159 @@
import frappe
import datetime
import json
import pytz
WEEKDAYS = ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"]
no_cache = 1
def get_context(context):
is_enabled = frappe.db.get_single_value('Appointment Booking Settings', 'enable_scheduling')
if is_enabled:
return context
else:
frappe.local.flags.redirect_location = '/404'
raise frappe.Redirect
@frappe.whitelist(allow_guest=True)
def get_appointment_settings():
settings = frappe.get_doc('Appointment Booking Settings')
settings.holiday_list = frappe.get_doc('Holiday List', settings.holiday_list)
return settings
@frappe.whitelist(allow_guest=True)
def get_timezones():
from babel.dates import get_timezone, get_timezone_name, Locale
from frappe.utils.momentjs import get_all_timezones
translated_dict = {}
locale = Locale.parse(frappe.local.lang, sep="-")
for tz in get_all_timezones():
timezone_name = get_timezone_name(get_timezone(tz), locale=locale, width='short')
if timezone_name:
translated_dict[tz] = timezone_name + ' - ' + tz
return translated_dict
@frappe.whitelist(allow_guest=True)
def get_appointment_slots(date, timezone):
# Convert query to local timezones
format_string = '%Y-%m-%d %H:%M:%S'
query_start_time = datetime.datetime.strptime(date + ' 00:00:00', format_string)
query_end_time = datetime.datetime.strptime(date + ' 23:59:59', format_string)
query_start_time = convert_to_system_timezone(timezone, query_start_time)
query_end_time = convert_to_system_timezone(timezone, query_end_time)
now = convert_to_guest_timezone(timezone, datetime.datetime.now())
# Database queries
settings = frappe.get_doc('Appointment Booking Settings')
holiday_list = frappe.get_doc('Holiday List', settings.holiday_list)
timeslots = get_available_slots_between(query_start_time, query_end_time, settings)
# Filter and convert timeslots
converted_timeslots = []
for timeslot in timeslots:
converted_timeslot = convert_to_guest_timezone(timezone, timeslot)
# Check if holiday
if _is_holiday(converted_timeslot.date(), holiday_list):
converted_timeslots.append(dict(time=converted_timeslot, availability=False))
continue
# Check availability
if check_availabilty(timeslot, settings) and converted_timeslot >= now:
converted_timeslots.append(dict(time=converted_timeslot, availability=True))
else:
converted_timeslots.append(dict(time=converted_timeslot, availability=False))
date_required = datetime.datetime.strptime(date + ' 00:00:00', format_string).date()
converted_timeslots = filter_timeslots(date_required, converted_timeslots)
return converted_timeslots
def get_available_slots_between(query_start_time, query_end_time, settings):
records = _get_records(query_start_time, query_end_time, settings)
timeslots = []
appointment_duration = datetime.timedelta(
minutes=settings.appointment_duration)
for record in records:
if record.day_of_week == WEEKDAYS[query_start_time.weekday()]:
current_time = _deltatime_to_datetime(query_start_time, record.from_time)
end_time = _deltatime_to_datetime(query_start_time, record.to_time)
else:
current_time = _deltatime_to_datetime(query_end_time, record.from_time)
end_time = _deltatime_to_datetime(query_end_time, record.to_time)
while current_time + appointment_duration <= end_time:
timeslots.append(current_time)
current_time += appointment_duration
return timeslots
@frappe.whitelist(allow_guest=True)
def create_appointment(date, time, tz, contact):
format_string = '%Y-%m-%d %H:%M:%S%z'
scheduled_time = datetime.datetime.strptime(date + " " + time, format_string)
# Strip tzinfo from datetime objects since it's handled by the doctype
scheduled_time = scheduled_time.replace(tzinfo = None)
scheduled_time = convert_to_system_timezone(tz, scheduled_time)
scheduled_time = scheduled_time.replace(tzinfo = None)
# Create a appointment document from form
appointment = frappe.new_doc('Appointment')
appointment.scheduled_time = scheduled_time
contact = json.loads(contact)
appointment.customer_name = contact.get('name', None)
appointment.customer_phone_number = contact.get('number', None)
appointment.customer_skype = contact.get('skype', None)
appointment.customer_details = contact.get('notes', None)
appointment.customer_email = contact.get('email', None)
appointment.status = 'Open'
appointment.insert()
return appointment
# Helper Functions
def filter_timeslots(date, timeslots):
filtered_timeslots = []
for timeslot in timeslots:
if(timeslot['time'].date() == date):
filtered_timeslots.append(timeslot)
return filtered_timeslots
def convert_to_guest_timezone(guest_tz, datetimeobject):
guest_tz = pytz.timezone(guest_tz)
local_timezone = pytz.timezone(frappe.utils.get_time_zone())
datetimeobject = local_timezone.localize(datetimeobject)
datetimeobject = datetimeobject.astimezone(guest_tz)
return datetimeobject
def convert_to_system_timezone(guest_tz,datetimeobject):
guest_tz = pytz.timezone(guest_tz)
datetimeobject = guest_tz.localize(datetimeobject)
system_tz = pytz.timezone(frappe.utils.get_time_zone())
datetimeobject = datetimeobject.astimezone(system_tz)
return datetimeobject
def check_availabilty(timeslot, settings):
return frappe.db.count('Appointment', {'scheduled_time': timeslot}) < settings.number_of_agents
def _is_holiday(date, holiday_list):
for holiday in holiday_list.holidays:
if holiday.holiday_date == date:
return True
return False
def _get_records(start_time, end_time, settings):
records = []
for record in settings.availability_of_slots:
if record.day_of_week == WEEKDAYS[start_time.weekday()] or record.day_of_week == WEEKDAYS[end_time.weekday()]:
records.append(record)
return records
def _deltatime_to_datetime(date, deltatime):
time = (datetime.datetime.min + deltatime).time()
return datetime.datetime.combine(date.date(), time)
def _datetime_to_deltatime(date_time):
midnight = datetime.datetime.combine(date_time.date(), datetime.time.min)
return (date_time-midnight)

View File

@ -0,0 +1,18 @@
{% extends "templates/web.html" %}
{% block title %}
{{ _("Verify Email") }}
{% endblock%}
{% block page_content %}
{% if success==True %}
<div class="alert alert-success">
Your email has been verified and your appointment has been scheduled
</div>
{% else %}
<div class="alert alert-danger">
Verification failed please check the link
</div>
{% endif %}
{% endblock%}

View File

@ -0,0 +1,20 @@
import frappe
from frappe.utils.verified_command import verify_request
@frappe.whitelist(allow_guest=True)
def get_context(context):
if not verify_request():
context.success = False
return context
email = frappe.form_dict['email']
appointment_name = frappe.form_dict['appointment']
if email and appointment_name:
appointment = frappe.get_doc('Appointment',appointment_name)
appointment.set_verified(email)
context.success = True
return context
else:
context.success = False
return context