Merge branch 'develop' into fix-item-tax-template-patch
This commit is contained in:
commit
4766732b56
2
.flake8
2
.flake8
@ -29,6 +29,8 @@ ignore =
|
||||
B950,
|
||||
W191,
|
||||
E124, # closing bracket, irritating while writing QB code
|
||||
E131, # continuation line unaligned for hanging indent
|
||||
E123, # closing bracket does not match indentation of opening bracket's line
|
||||
|
||||
max-line-length = 200
|
||||
exclude=.github/helper/semgrep_rules
|
||||
|
@ -134,7 +134,8 @@
|
||||
{
|
||||
"fieldname": "allocated_amount",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Allocated Amount"
|
||||
"label": "Allocated Amount",
|
||||
"options": "currency"
|
||||
},
|
||||
{
|
||||
"fieldname": "amended_from",
|
||||
@ -152,7 +153,8 @@
|
||||
{
|
||||
"fieldname": "unallocated_amount",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Unallocated Amount"
|
||||
"label": "Unallocated Amount",
|
||||
"options": "currency"
|
||||
},
|
||||
{
|
||||
"fieldname": "party_section",
|
||||
@ -192,10 +194,11 @@
|
||||
],
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2021-04-14 17:31:58.963529",
|
||||
"modified": "2022-03-21 19:05:04.208222",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Bank Transaction",
|
||||
"naming_rule": "By \"Naming Series\" field",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
@ -242,6 +245,7 @@
|
||||
],
|
||||
"sort_field": "date",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"title_field": "bank_account",
|
||||
"track_changes": 1
|
||||
}
|
@ -110,13 +110,12 @@
|
||||
"description": "Reference number of the invoice from the previous system",
|
||||
"fieldname": "invoice_number",
|
||||
"fieldtype": "Data",
|
||||
"in_list_view": 1,
|
||||
"label": "Invoice Number"
|
||||
}
|
||||
],
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2021-12-17 19:25:06.053187",
|
||||
"modified": "2022-03-21 19:31:45.382656",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Opening Invoice Creation Tool Item",
|
||||
@ -125,5 +124,6 @@
|
||||
"quick_entry": 1,
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
@ -264,7 +264,6 @@
|
||||
"print_hide": 1
|
||||
},
|
||||
{
|
||||
"allow_on_submit": 1,
|
||||
"default": "0",
|
||||
"fieldname": "is_return",
|
||||
"fieldtype": "Check",
|
||||
@ -1573,7 +1572,7 @@
|
||||
"icon": "fa fa-file-text",
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2021-10-05 12:11:53.871828",
|
||||
"modified": "2022-03-22 13:00:24.166684",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "POS Invoice",
|
||||
@ -1623,6 +1622,7 @@
|
||||
"show_name_in_global_search": 1,
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"timeline_field": "customer",
|
||||
"title_field": "title",
|
||||
"track_changes": 1,
|
||||
|
@ -16,7 +16,11 @@ from erpnext.accounts.doctype.sales_invoice.sales_invoice import (
|
||||
)
|
||||
from erpnext.accounts.party import get_due_date, get_party_account
|
||||
from erpnext.stock.doctype.batch.batch import get_batch_qty, get_pos_reserved_batch_qty
|
||||
from erpnext.stock.doctype.serial_no.serial_no import get_pos_reserved_serial_nos, get_serial_nos
|
||||
from erpnext.stock.doctype.serial_no.serial_no import (
|
||||
get_delivered_serial_nos,
|
||||
get_pos_reserved_serial_nos,
|
||||
get_serial_nos,
|
||||
)
|
||||
|
||||
|
||||
class POSInvoice(SalesInvoice):
|
||||
@ -145,12 +149,7 @@ class POSInvoice(SalesInvoice):
|
||||
.format(item.idx, bold_invalid_batch_no, bold_item_name, bold_extra_batch_qty_needed), title=_("Item Unavailable"))
|
||||
|
||||
def validate_delivered_serial_nos(self, item):
|
||||
serial_nos = get_serial_nos(item.serial_no)
|
||||
delivered_serial_nos = frappe.db.get_list('Serial No', {
|
||||
'item_code': item.item_code,
|
||||
'name': ['in', serial_nos],
|
||||
'sales_invoice': ['is', 'set']
|
||||
}, pluck='name')
|
||||
delivered_serial_nos = get_delivered_serial_nos(item.serial_no)
|
||||
|
||||
if delivered_serial_nos:
|
||||
bold_delivered_serial_nos = frappe.bold(', '.join(delivered_serial_nos))
|
||||
@ -172,10 +171,14 @@ class POSInvoice(SalesInvoice):
|
||||
frappe.throw(error_msg, title=_("Invalid Item"), as_list=True)
|
||||
|
||||
def validate_stock_availablility(self):
|
||||
if self.is_return:
|
||||
return
|
||||
|
||||
if self.docstatus.is_draft() and not frappe.db.get_value('POS Profile', self.pos_profile, 'validate_stock_on_save'):
|
||||
return
|
||||
|
||||
from erpnext.stock.stock_ledger import is_negative_stock_allowed
|
||||
|
||||
if self.is_return or self.docstatus != 1:
|
||||
return
|
||||
for d in self.get('items'):
|
||||
is_service_item = not (frappe.db.get_value('Item', d.get('item_code'), 'is_stock_item'))
|
||||
if is_service_item:
|
||||
@ -485,16 +488,15 @@ class POSInvoice(SalesInvoice):
|
||||
"payment_account": pay.account,
|
||||
}, ["name"])
|
||||
|
||||
args = {
|
||||
'doctype': 'Payment Request',
|
||||
filters = {
|
||||
'reference_doctype': 'POS Invoice',
|
||||
'reference_name': self.name,
|
||||
'payment_gateway_account': payment_gateway_account,
|
||||
'email_to': self.contact_mobile
|
||||
}
|
||||
pr = frappe.db.exists(args)
|
||||
pr = frappe.db.get_value('Payment Request', filters=filters)
|
||||
if pr:
|
||||
return frappe.get_doc('Payment Request', pr[0][0])
|
||||
return frappe.get_doc('Payment Request', pr)
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_stock_availability(item_code, warehouse):
|
||||
|
@ -340,6 +340,7 @@ class TestPOSInvoice(unittest.TestCase):
|
||||
item=se.get("items")[0].item_code, rate=1000, do_not_save=1)
|
||||
|
||||
si.get("items")[0].serial_no = serial_nos[0]
|
||||
si.update_stock = 1
|
||||
si.insert()
|
||||
si.submit()
|
||||
|
||||
@ -610,6 +611,78 @@ class TestPOSInvoice(unittest.TestCase):
|
||||
pos_inv.delete()
|
||||
pr.delete()
|
||||
|
||||
def test_delivered_serial_no_case(self):
|
||||
from erpnext.accounts.doctype.pos_invoice_merge_log.test_pos_invoice_merge_log import (
|
||||
init_user_and_profile,
|
||||
)
|
||||
from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note
|
||||
from erpnext.stock.doctype.serial_no.test_serial_no import get_serial_nos
|
||||
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_serialized_item
|
||||
|
||||
frappe.db.savepoint('before_test_delivered_serial_no_case')
|
||||
try:
|
||||
se = make_serialized_item()
|
||||
serial_no = get_serial_nos(se.get("items")[0].serial_no)[0]
|
||||
|
||||
dn = create_delivery_note(
|
||||
item_code="_Test Serialized Item With Series", serial_no=serial_no
|
||||
)
|
||||
|
||||
delivery_document_no = frappe.db.get_value("Serial No", serial_no, "delivery_document_no")
|
||||
self.assertEquals(delivery_document_no, dn.name)
|
||||
|
||||
init_user_and_profile()
|
||||
|
||||
pos_inv = create_pos_invoice(
|
||||
item_code="_Test Serialized Item With Series",
|
||||
serial_no=serial_no,
|
||||
qty=1,
|
||||
rate=100,
|
||||
do_not_submit=True
|
||||
)
|
||||
|
||||
self.assertRaises(frappe.ValidationError, pos_inv.submit)
|
||||
|
||||
finally:
|
||||
frappe.db.rollback(save_point='before_test_delivered_serial_no_case')
|
||||
frappe.set_user("Administrator")
|
||||
|
||||
def test_returned_serial_no_case(self):
|
||||
from erpnext.accounts.doctype.pos_invoice_merge_log.test_pos_invoice_merge_log import (
|
||||
init_user_and_profile,
|
||||
)
|
||||
from erpnext.stock.doctype.serial_no.serial_no import get_pos_reserved_serial_nos
|
||||
from erpnext.stock.doctype.serial_no.test_serial_no import get_serial_nos
|
||||
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_serialized_item
|
||||
|
||||
frappe.db.savepoint('before_test_returned_serial_no_case')
|
||||
try:
|
||||
se = make_serialized_item()
|
||||
serial_no = get_serial_nos(se.get("items")[0].serial_no)[0]
|
||||
|
||||
init_user_and_profile()
|
||||
|
||||
pos_inv = create_pos_invoice(
|
||||
item_code="_Test Serialized Item With Series",
|
||||
serial_no=serial_no,
|
||||
qty=1,
|
||||
rate=100,
|
||||
)
|
||||
|
||||
pos_return = make_sales_return(pos_inv.name)
|
||||
pos_return.flags.ignore_validate = True
|
||||
pos_return.insert()
|
||||
pos_return.submit()
|
||||
|
||||
pos_reserved_serial_nos = get_pos_reserved_serial_nos({
|
||||
'item_code': '_Test Serialized Item With Series',
|
||||
'warehouse': '_Test Warehouse - _TC'
|
||||
})
|
||||
self.assertTrue(serial_no not in pos_reserved_serial_nos)
|
||||
|
||||
finally:
|
||||
frappe.db.rollback(save_point='before_test_returned_serial_no_case')
|
||||
frappe.set_user("Administrator")
|
||||
|
||||
def create_pos_invoice(**args):
|
||||
args = frappe._dict(args)
|
||||
|
@ -53,7 +53,7 @@ class POSInvoiceMergeLog(Document):
|
||||
frappe.throw(msg)
|
||||
|
||||
def on_submit(self):
|
||||
pos_invoice_docs = [frappe.get_doc("POS Invoice", d.pos_invoice) for d in self.pos_invoices]
|
||||
pos_invoice_docs = [frappe.get_cached_doc("POS Invoice", d.pos_invoice) for d in self.pos_invoices]
|
||||
|
||||
returns = [d for d in pos_invoice_docs if d.get('is_return') == 1]
|
||||
sales = [d for d in pos_invoice_docs if d.get('is_return') == 0]
|
||||
@ -70,7 +70,7 @@ class POSInvoiceMergeLog(Document):
|
||||
self.update_pos_invoices(pos_invoice_docs, sales_invoice, credit_note)
|
||||
|
||||
def on_cancel(self):
|
||||
pos_invoice_docs = [frappe.get_doc("POS Invoice", d.pos_invoice) for d in self.pos_invoices]
|
||||
pos_invoice_docs = [frappe.get_cached_doc("POS Invoice", d.pos_invoice) for d in self.pos_invoices]
|
||||
|
||||
self.update_pos_invoices(pos_invoice_docs)
|
||||
self.cancel_linked_invoices()
|
||||
@ -254,7 +254,7 @@ def get_all_unconsolidated_invoices():
|
||||
'docstatus': 1
|
||||
}
|
||||
pos_invoices = frappe.db.get_all('POS Invoice', filters=filters,
|
||||
fields=["name as pos_invoice", 'posting_date', 'grand_total', 'customer'])
|
||||
fields=["name as pos_invoice", 'posting_date', 'grand_total', 'customer', 'is_return', 'return_against'])
|
||||
|
||||
return pos_invoices
|
||||
|
||||
@ -294,17 +294,62 @@ def unconsolidate_pos_invoices(closing_entry):
|
||||
else:
|
||||
cancel_merge_logs(merge_logs, closing_entry)
|
||||
|
||||
def split_invoices(invoices):
|
||||
'''
|
||||
Splits invoices into multiple groups
|
||||
Use-case:
|
||||
If a serial no is sold and later it is returned
|
||||
then split the invoices such that the selling entry is merged first and then the return entry
|
||||
'''
|
||||
# Input
|
||||
# invoices = [
|
||||
# {'pos_invoice': 'Invoice with SR#1 & SR#2', 'is_return': 0},
|
||||
# {'pos_invoice': 'Invoice with SR#1', 'is_return': 1},
|
||||
# {'pos_invoice': 'Invoice with SR#2', 'is_return': 0}
|
||||
# ]
|
||||
# Output
|
||||
# _invoices = [
|
||||
# [{'pos_invoice': 'Invoice with SR#1 & SR#2', 'is_return': 0}],
|
||||
# [{'pos_invoice': 'Invoice with SR#1', 'is_return': 1}, {'pos_invoice': 'Invoice with SR#2', 'is_return': 0}],
|
||||
# ]
|
||||
|
||||
_invoices = []
|
||||
special_invoices = []
|
||||
pos_return_docs = [frappe.get_cached_doc("POS Invoice", d.pos_invoice) for d in invoices if d.is_return and d.return_against]
|
||||
for pos_invoice in pos_return_docs:
|
||||
for item in pos_invoice.items:
|
||||
if not item.serial_no:
|
||||
continue
|
||||
|
||||
return_against_is_added = any(d for d in _invoices if d.pos_invoice == pos_invoice.return_against)
|
||||
if return_against_is_added:
|
||||
break
|
||||
|
||||
return_against_is_consolidated = frappe.db.get_value('POS Invoice', pos_invoice.return_against, 'status', cache=True) == 'Consolidated'
|
||||
if return_against_is_consolidated:
|
||||
break
|
||||
|
||||
pos_invoice_row = [d for d in invoices if d.pos_invoice == pos_invoice.return_against]
|
||||
_invoices.append(pos_invoice_row)
|
||||
special_invoices.append(pos_invoice.return_against)
|
||||
break
|
||||
|
||||
_invoices.append([d for d in invoices if d.pos_invoice not in special_invoices])
|
||||
|
||||
return _invoices
|
||||
|
||||
def create_merge_logs(invoice_by_customer, closing_entry=None):
|
||||
try:
|
||||
for customer, invoices in invoice_by_customer.items():
|
||||
merge_log = frappe.new_doc('POS Invoice Merge Log')
|
||||
merge_log.posting_date = getdate(closing_entry.get('posting_date')) if closing_entry else nowdate()
|
||||
merge_log.customer = customer
|
||||
merge_log.pos_closing_entry = closing_entry.get('name') if closing_entry else None
|
||||
for _invoices in split_invoices(invoices):
|
||||
merge_log = frappe.new_doc('POS Invoice Merge Log')
|
||||
merge_log.posting_date = getdate(closing_entry.get('posting_date')) if closing_entry else nowdate()
|
||||
merge_log.customer = customer
|
||||
merge_log.pos_closing_entry = closing_entry.get('name') if closing_entry else None
|
||||
|
||||
merge_log.set('pos_invoices', invoices)
|
||||
merge_log.save(ignore_permissions=True)
|
||||
merge_log.submit()
|
||||
merge_log.set('pos_invoices', _invoices)
|
||||
merge_log.save(ignore_permissions=True)
|
||||
merge_log.submit()
|
||||
|
||||
if closing_entry:
|
||||
closing_entry.set_status(update=True, status='Submitted')
|
||||
|
@ -382,6 +382,68 @@ class TestPOSInvoiceMergeLog(unittest.TestCase):
|
||||
consolidated_invoice = frappe.get_doc('Sales Invoice', inv.consolidated_invoice)
|
||||
self.assertEqual(consolidated_invoice.rounding_adjustment, 1)
|
||||
|
||||
finally:
|
||||
frappe.set_user("Administrator")
|
||||
frappe.db.sql("delete from `tabPOS Profile`")
|
||||
frappe.db.sql("delete from `tabPOS Invoice`")
|
||||
|
||||
def test_serial_no_case_1(self):
|
||||
'''
|
||||
Create a POS Invoice with serial no
|
||||
Create a Return Invoice with serial no
|
||||
Create a POS Invoice with serial no again
|
||||
Consolidate the invoices
|
||||
|
||||
The first POS Invoice should be consolidated with a separate single Merge Log
|
||||
The second and third POS Invoice should be consolidated with a single Merge Log
|
||||
'''
|
||||
|
||||
from erpnext.stock.doctype.serial_no.test_serial_no import get_serial_nos
|
||||
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_serialized_item
|
||||
|
||||
frappe.db.sql("delete from `tabPOS Invoice`")
|
||||
|
||||
try:
|
||||
se = make_serialized_item()
|
||||
serial_no = get_serial_nos(se.get("items")[0].serial_no)[0]
|
||||
|
||||
init_user_and_profile()
|
||||
|
||||
pos_inv = create_pos_invoice(
|
||||
item_code="_Test Serialized Item With Series",
|
||||
serial_no=serial_no,
|
||||
qty=1,
|
||||
rate=100,
|
||||
do_not_submit=1
|
||||
)
|
||||
pos_inv.append('payments', {
|
||||
'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': 100
|
||||
})
|
||||
pos_inv.submit()
|
||||
|
||||
pos_inv_cn = make_sales_return(pos_inv.name)
|
||||
pos_inv_cn.paid_amount = -100
|
||||
pos_inv_cn.submit()
|
||||
|
||||
pos_inv2 = create_pos_invoice(
|
||||
item_code="_Test Serialized Item With Series",
|
||||
serial_no=serial_no,
|
||||
qty=1,
|
||||
rate=100,
|
||||
do_not_submit=1
|
||||
)
|
||||
pos_inv2.append('payments', {
|
||||
'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': 100
|
||||
})
|
||||
pos_inv2.submit()
|
||||
|
||||
consolidate_pos_invoices()
|
||||
|
||||
pos_inv.load_from_db()
|
||||
pos_inv2.load_from_db()
|
||||
|
||||
self.assertNotEqual(pos_inv.consolidated_invoice, pos_inv2.consolidated_invoice)
|
||||
|
||||
finally:
|
||||
frappe.set_user("Administrator")
|
||||
frappe.db.sql("delete from `tabPOS Profile`")
|
||||
|
@ -9,7 +9,9 @@
|
||||
"posting_date",
|
||||
"column_break_3",
|
||||
"customer",
|
||||
"grand_total"
|
||||
"grand_total",
|
||||
"is_return",
|
||||
"return_against"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
@ -48,11 +50,27 @@
|
||||
"in_list_view": 1,
|
||||
"label": "Amount",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fetch_from": "pos_invoice.is_return",
|
||||
"fieldname": "is_return",
|
||||
"fieldtype": "Check",
|
||||
"label": "Is Return",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fetch_from": "pos_invoice.return_against",
|
||||
"fieldname": "return_against",
|
||||
"fieldtype": "Link",
|
||||
"label": "Return Against",
|
||||
"options": "POS Invoice",
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2020-05-29 15:08:42.194979",
|
||||
"modified": "2022-03-24 13:32:02.366257",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "POS Invoice Reference",
|
||||
@ -61,5 +79,6 @@
|
||||
"quick_entry": 1,
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
@ -22,6 +22,7 @@
|
||||
"hide_images",
|
||||
"hide_unavailable_items",
|
||||
"auto_add_item_to_cart",
|
||||
"validate_stock_on_save",
|
||||
"column_break_16",
|
||||
"update_stock",
|
||||
"ignore_pricing_rule",
|
||||
@ -351,6 +352,12 @@
|
||||
{
|
||||
"fieldname": "column_break_25",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "validate_stock_on_save",
|
||||
"fieldtype": "Check",
|
||||
"label": "Validate Stock on Save"
|
||||
}
|
||||
],
|
||||
"icon": "icon-cog",
|
||||
@ -378,10 +385,11 @@
|
||||
"link_fieldname": "pos_profile"
|
||||
}
|
||||
],
|
||||
"modified": "2021-10-14 14:17:00.469298",
|
||||
"modified": "2022-03-21 13:29:28.480533",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "POS Profile",
|
||||
"naming_rule": "Set by user",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
@ -404,5 +412,6 @@
|
||||
}
|
||||
],
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC"
|
||||
"sort_order": "DESC",
|
||||
"states": []
|
||||
}
|
@ -1711,6 +1711,7 @@ def make_delivery_note(source_name, target_doc=None):
|
||||
}
|
||||
}, target_doc, set_missing_values)
|
||||
|
||||
doclist.set_onload('ignore_price_list', True)
|
||||
return doclist
|
||||
|
||||
@frappe.whitelist()
|
||||
|
@ -442,6 +442,8 @@ def make_purchase_receipt(source_name, target_doc=None):
|
||||
}
|
||||
}, target_doc, set_missing_values)
|
||||
|
||||
doc.set_onload('ignore_price_list', True)
|
||||
|
||||
return doc
|
||||
|
||||
@frappe.whitelist()
|
||||
@ -509,6 +511,7 @@ def get_mapped_purchase_invoice(source_name, target_doc=None, ignore_permissions
|
||||
|
||||
doc = get_mapped_doc("Purchase Order", source_name, fields,
|
||||
target_doc, postprocess, ignore_permissions=ignore_permissions)
|
||||
doc.set_onload('ignore_price_list', True)
|
||||
|
||||
return doc
|
||||
|
||||
|
@ -139,6 +139,7 @@ def make_purchase_order(source_name, target_doc=None):
|
||||
},
|
||||
}, target_doc, set_missing_values)
|
||||
|
||||
doclist.set_onload('ignore_price_list', True)
|
||||
return doclist
|
||||
|
||||
@frappe.whitelist()
|
||||
|
@ -168,7 +168,7 @@ def tax_account_query(doctype, txt, searchfield, start, page_len, filters):
|
||||
{account_type_condition}
|
||||
AND is_group = 0
|
||||
AND company = %(company)s
|
||||
AND account_currency = %(currency)s
|
||||
AND (account_currency = %(currency)s or ifnull(account_currency, '') = '')
|
||||
AND `{searchfield}` LIKE %(txt)s
|
||||
{mcond}
|
||||
ORDER BY idx DESC, name
|
||||
|
@ -399,6 +399,8 @@ def make_return_doc(doctype, source_name, target_doc=None):
|
||||
}
|
||||
}, target_doc, set_missing_values)
|
||||
|
||||
doclist.set_onload('ignore_price_list', True)
|
||||
|
||||
return doclist
|
||||
|
||||
def get_rate_for_return(voucher_type, voucher_no, item_code, return_against=None,
|
||||
|
@ -114,20 +114,16 @@ class calculate_taxes_and_totals(object):
|
||||
for item in self.doc.get("items"):
|
||||
self.doc.round_floats_in(item)
|
||||
|
||||
if not item.rate:
|
||||
item.rate = item.price_list_rate
|
||||
|
||||
if item.discount_percentage == 100:
|
||||
item.rate = 0.0
|
||||
elif item.price_list_rate:
|
||||
if item.pricing_rules or abs(item.discount_percentage) > 0:
|
||||
if not item.rate or (item.pricing_rules and item.discount_percentage > 0):
|
||||
item.rate = flt(item.price_list_rate *
|
||||
(1.0 - (item.discount_percentage / 100.0)), item.precision("rate"))
|
||||
|
||||
if abs(item.discount_percentage) > 0:
|
||||
item.discount_amount = item.price_list_rate * (item.discount_percentage / 100.0)
|
||||
item.discount_amount = item.price_list_rate * (item.discount_percentage / 100.0)
|
||||
|
||||
elif item.discount_amount or item.pricing_rules:
|
||||
elif item.discount_amount and item.pricing_rules:
|
||||
item.rate = item.price_list_rate - item.discount_amount
|
||||
|
||||
if item.doctype in ['Quotation Item', 'Sales Order Item', 'Delivery Note Item', 'Sales Invoice Item',
|
||||
|
@ -225,9 +225,7 @@ def _check_agent_availability(agent_email, scheduled_time):
|
||||
|
||||
|
||||
def _get_employee_from_user(user):
|
||||
employee_docname = frappe.db.exists(
|
||||
{'doctype': 'Employee', 'user_id': user})
|
||||
employee_docname = frappe.db.get_value('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 frappe.get_doc('Employee', employee_docname)
|
||||
return None
|
||||
|
@ -8,50 +8,44 @@ import frappe
|
||||
|
||||
|
||||
def create_test_lead():
|
||||
test_lead = frappe.db.exists({'doctype': 'Lead', 'email_id':'test@example.com'})
|
||||
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
|
||||
test_lead = frappe.db.get_value("Lead", {"email_id": "test@example.com"})
|
||||
if test_lead:
|
||||
return frappe.get_doc("Lead", test_lead)
|
||||
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
|
||||
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
|
||||
test_appointment = test_lead = None
|
||||
|
||||
def setUp(self):
|
||||
self.test_lead = create_test_lead()
|
||||
self.test_appointment = create_test_appointments()
|
||||
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_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)
|
||||
def test_lead_linked(self):
|
||||
lead = frappe.get_doc("Lead", self.test_lead.name)
|
||||
self.assertIsNotNone(lead)
|
||||
|
@ -5,6 +5,12 @@ frappe.ui.form.on('Website Item', {
|
||||
onload: function(frm) {
|
||||
// should never check Private
|
||||
frm.fields_dict["website_image"].df.is_private = 0;
|
||||
|
||||
frm.set_query("website_warehouse", () => {
|
||||
return {
|
||||
filters: {"is_group": 0}
|
||||
};
|
||||
});
|
||||
},
|
||||
|
||||
image: function() {
|
||||
|
@ -418,6 +418,22 @@ erpnext.ProductView = class {
|
||||
|
||||
me.change_route_with_filters();
|
||||
});
|
||||
|
||||
// bind filter lookup input box
|
||||
$('.filter-lookup-input').on('keydown', frappe.utils.debounce((e) => {
|
||||
const $input = $(e.target);
|
||||
const keyword = ($input.val() || '').toLowerCase();
|
||||
const $filter_options = $input.next('.filter-options');
|
||||
|
||||
$filter_options.find('.filter-lookup-wrapper').show();
|
||||
$filter_options.find('.filter-lookup-wrapper').each((i, el) => {
|
||||
const $el = $(el);
|
||||
const value = $el.data('value').toLowerCase();
|
||||
if (!value.includes(keyword)) {
|
||||
$el.hide();
|
||||
}
|
||||
});
|
||||
}, 300));
|
||||
}
|
||||
|
||||
change_route_with_filters() {
|
||||
|
@ -15,6 +15,7 @@ class TestVariantSelector(FrappeTestCase):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
template_item = make_item("Test-Tshirt-Temp", {
|
||||
"has_variant": 1,
|
||||
"variant_based_on": "Item Attribute",
|
||||
|
@ -3,7 +3,6 @@
|
||||
|
||||
|
||||
import frappe
|
||||
from erpnext.setup.utils import insert_record
|
||||
|
||||
|
||||
def setup_education():
|
||||
@ -13,6 +12,21 @@ def setup_education():
|
||||
return
|
||||
create_academic_sessions()
|
||||
|
||||
|
||||
def insert_record(records):
|
||||
for r in records:
|
||||
doc = frappe.new_doc(r.get("doctype"))
|
||||
doc.update(r)
|
||||
try:
|
||||
doc.insert(ignore_permissions=True)
|
||||
except frappe.DuplicateEntryError as e:
|
||||
# pass DuplicateEntryError and continue
|
||||
if e.args and e.args[0]==doc.doctype and e.args[1]==doc.name:
|
||||
# make sure DuplicateEntryError is for the exact same doc and not a related doc
|
||||
pass
|
||||
else:
|
||||
raise
|
||||
|
||||
def create_academic_sessions():
|
||||
data = [
|
||||
{"doctype": "Academic Year", "academic_year_name": "2015-16"},
|
||||
|
@ -42,10 +42,18 @@ class ExpenseClaim(AccountsController):
|
||||
"2": "Cancelled"
|
||||
}[cstr(self.docstatus or 0)]
|
||||
|
||||
paid_amount = flt(self.total_amount_reimbursed) + flt(self.total_advance_amount)
|
||||
precision = self.precision("grand_total")
|
||||
if (self.is_paid or (flt(self.total_sanctioned_amount) > 0 and self.docstatus == 1
|
||||
and flt(self.grand_total, precision) == flt(paid_amount, precision))) and self.approval_status == 'Approved':
|
||||
|
||||
if (
|
||||
# set as paid
|
||||
self.is_paid
|
||||
or (flt(self.total_sanctioned_amount > 0) and (
|
||||
# grand total is reimbursed
|
||||
(self.docstatus == 1 and flt(self.grand_total, precision) == flt(self.total_amount_reimbursed, precision))
|
||||
# grand total (to be paid) is 0 since linked advances already cover the claimed amount
|
||||
or (flt(self.grand_total, precision) == 0)
|
||||
))
|
||||
) and self.approval_status == "Approved":
|
||||
status = "Paid"
|
||||
elif flt(self.total_sanctioned_amount) > 0 and self.docstatus == 1 and self.approval_status == 'Approved':
|
||||
status = "Unpaid"
|
||||
|
@ -72,6 +72,72 @@ class TestExpenseClaim(unittest.TestCase):
|
||||
expense_claim = frappe.get_doc("Expense Claim", expense_claim.name)
|
||||
self.assertEqual(expense_claim.status, "Unpaid")
|
||||
|
||||
# expense claim without any sanctioned amount should not have status as Paid
|
||||
claim = make_expense_claim(payable_account, 1000, 0, "_Test Company", "Travel Expenses - _TC")
|
||||
self.assertEqual(claim.total_sanctioned_amount, 0)
|
||||
self.assertEqual(claim.status, "Submitted")
|
||||
|
||||
# no gl entries created
|
||||
gl_entry = frappe.get_all('GL Entry', {'voucher_type': 'Expense Claim', 'voucher_no': claim.name})
|
||||
self.assertEqual(len(gl_entry), 0)
|
||||
|
||||
def test_expense_claim_against_fully_paid_advances(self):
|
||||
from erpnext.hr.doctype.employee_advance.test_employee_advance import (
|
||||
get_advances_for_claim,
|
||||
make_employee_advance,
|
||||
make_payment_entry,
|
||||
)
|
||||
|
||||
frappe.db.delete("Employee Advance")
|
||||
|
||||
payable_account = get_payable_account("_Test Company")
|
||||
claim = make_expense_claim(payable_account, 1000, 1000, "_Test Company", "Travel Expenses - _TC", do_not_submit=True)
|
||||
|
||||
advance = make_employee_advance(claim.employee)
|
||||
pe = make_payment_entry(advance)
|
||||
pe.submit()
|
||||
|
||||
# claim for already paid out advances
|
||||
claim = get_advances_for_claim(claim, advance.name)
|
||||
claim.save()
|
||||
claim.submit()
|
||||
|
||||
self.assertEqual(claim.grand_total, 0)
|
||||
self.assertEqual(claim.status, "Paid")
|
||||
|
||||
def test_expense_claim_partially_paid_via_advance(self):
|
||||
from erpnext.hr.doctype.employee_advance.test_employee_advance import (
|
||||
get_advances_for_claim,
|
||||
make_employee_advance,
|
||||
)
|
||||
from erpnext.hr.doctype.employee_advance.test_employee_advance import (
|
||||
make_payment_entry as make_advance_payment,
|
||||
)
|
||||
|
||||
frappe.db.delete("Employee Advance")
|
||||
|
||||
payable_account = get_payable_account("_Test Company")
|
||||
claim = make_expense_claim(payable_account, 1000, 1000, "_Test Company", "Travel Expenses - _TC", do_not_submit=True)
|
||||
|
||||
# link advance for partial amount
|
||||
advance = make_employee_advance(claim.employee, {'advance_amount': 500})
|
||||
pe = make_advance_payment(advance)
|
||||
pe.submit()
|
||||
|
||||
claim = get_advances_for_claim(claim, advance.name)
|
||||
claim.save()
|
||||
claim.submit()
|
||||
|
||||
self.assertEqual(claim.grand_total, 500)
|
||||
self.assertEqual(claim.status, "Unpaid")
|
||||
|
||||
# reimburse remaning amount
|
||||
make_payment_entry(claim, payable_account, 500)
|
||||
claim.reload()
|
||||
|
||||
self.assertEqual(claim.total_amount_reimbursed, 500)
|
||||
self.assertEqual(claim.status, "Paid")
|
||||
|
||||
def test_expense_claim_gl_entry(self):
|
||||
payable_account = get_payable_account(company_name)
|
||||
taxes = generate_taxes()
|
||||
|
@ -25,6 +25,7 @@ from erpnext.hr.doctype.leave_application.leave_application import (
|
||||
LeaveDayBlockedError,
|
||||
NotAnOptionalHoliday,
|
||||
OverlapError,
|
||||
get_leave_allocation_records,
|
||||
get_leave_balance_on,
|
||||
get_leave_details,
|
||||
)
|
||||
@ -882,6 +883,27 @@ class TestLeaveApplication(unittest.TestCase):
|
||||
self.assertEqual(leave_allocation['leaves_pending_approval'], 1)
|
||||
self.assertEqual(leave_allocation['remaining_leaves'], 26)
|
||||
|
||||
@set_holiday_list('Salary Slip Test Holiday List', '_Test Company')
|
||||
def test_get_leave_allocation_records(self):
|
||||
employee = get_employee()
|
||||
leave_type = create_leave_type(
|
||||
leave_type_name="_Test_CF_leave_expiry",
|
||||
is_carry_forward=1,
|
||||
expire_carry_forwarded_leaves_after_days=90)
|
||||
leave_type.insert()
|
||||
|
||||
leave_alloc = create_carry_forwarded_allocation(employee, leave_type)
|
||||
details = get_leave_allocation_records(employee.name, getdate(), leave_type.name)
|
||||
expected_data = {
|
||||
"from_date": getdate(leave_alloc.from_date),
|
||||
"to_date": getdate(leave_alloc.to_date),
|
||||
"total_leaves_allocated": 30.0,
|
||||
"unused_leaves": 15.0,
|
||||
"new_leaves_allocated": 15.0,
|
||||
"leave_type": leave_type.name
|
||||
}
|
||||
self.assertEqual(details.get(leave_type.name), expected_data)
|
||||
|
||||
|
||||
def create_carry_forwarded_allocation(employee, leave_type):
|
||||
# initial leave allocation
|
||||
@ -903,6 +925,8 @@ def create_carry_forwarded_allocation(employee, leave_type):
|
||||
carry_forward=1)
|
||||
leave_allocation.submit()
|
||||
|
||||
return leave_allocation
|
||||
|
||||
def make_allocation_record(employee=None, leave_type=None, from_date=None, to_date=None, carry_forward=False, leaves=None):
|
||||
allocation = frappe.get_doc({
|
||||
"doctype": "Leave Allocation",
|
||||
@ -931,12 +955,9 @@ def set_leave_approver():
|
||||
dept_doc.save(ignore_permissions=True)
|
||||
|
||||
def get_leave_period():
|
||||
leave_period_name = frappe.db.exists({
|
||||
"doctype": "Leave Period",
|
||||
"company": "_Test Company"
|
||||
})
|
||||
leave_period_name = frappe.db.get_value("Leave Period", {"company": "_Test Company"})
|
||||
if leave_period_name:
|
||||
return frappe.get_doc("Leave Period", leave_period_name[0][0])
|
||||
return frappe.get_doc("Leave Period", leave_period_name)
|
||||
else:
|
||||
return frappe.get_doc(dict(
|
||||
name = 'Test Leave Period',
|
||||
|
@ -500,6 +500,9 @@ class JobCard(Document):
|
||||
2: "Cancelled"
|
||||
}[self.docstatus or 0]
|
||||
|
||||
if self.for_quantity <= self.transferred_qty:
|
||||
self.status = 'Material Transferred'
|
||||
|
||||
if self.time_logs:
|
||||
self.status = 'Work In Progress'
|
||||
|
||||
@ -507,10 +510,6 @@ class JobCard(Document):
|
||||
(self.for_quantity <= self.total_completed_qty or not self.items)):
|
||||
self.status = 'Completed'
|
||||
|
||||
if self.status != 'Completed':
|
||||
if self.for_quantity <= self.transferred_qty:
|
||||
self.status = 'Material Transferred'
|
||||
|
||||
if update_status:
|
||||
self.db_set('status', self.status)
|
||||
|
||||
|
@ -169,6 +169,7 @@ class TestJobCard(FrappeTestCase):
|
||||
|
||||
job_card_name = frappe.db.get_value("Job Card", {'work_order': self.work_order.name})
|
||||
job_card = frappe.get_doc("Job Card", job_card_name)
|
||||
self.assertEqual(job_card.status, "Open")
|
||||
|
||||
# fully transfer both RMs
|
||||
transfer_entry_1 = make_stock_entry_from_jc(job_card_name)
|
||||
|
@ -2,6 +2,13 @@
|
||||
// For license information, please see license.txt
|
||||
|
||||
frappe.ui.form.on('Production Plan', {
|
||||
|
||||
before_save: function(frm) {
|
||||
// preserve temporary names on production plan item to re-link sub-assembly items
|
||||
frm.doc.po_items.forEach(item => {
|
||||
item.temporary_name = item.name;
|
||||
});
|
||||
},
|
||||
setup: function(frm) {
|
||||
frm.custom_make_buttons = {
|
||||
'Work Order': 'Work Order / Subcontract PO',
|
||||
|
@ -351,7 +351,6 @@
|
||||
"hide_border": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "get_items_from",
|
||||
"fieldname": "sub_assembly_items",
|
||||
"fieldtype": "Table",
|
||||
"label": "Sub Assembly Items",
|
||||
@ -385,7 +384,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2022-03-14 03:56:23.209247",
|
||||
"modified": "2022-03-25 09:15:25.017664",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Manufacturing",
|
||||
"name": "Production Plan",
|
||||
|
@ -32,6 +32,7 @@ class ProductionPlan(Document):
|
||||
self.set_pending_qty_in_row_without_reference()
|
||||
self.calculate_total_planned_qty()
|
||||
self.set_status()
|
||||
self._rename_temporary_references()
|
||||
|
||||
def set_pending_qty_in_row_without_reference(self):
|
||||
"Set Pending Qty in independent rows (not from SO or MR)."
|
||||
@ -57,6 +58,18 @@ class ProductionPlan(Document):
|
||||
if not flt(d.planned_qty):
|
||||
frappe.throw(_("Please enter Planned Qty for Item {0} at row {1}").format(d.item_code, d.idx))
|
||||
|
||||
def _rename_temporary_references(self):
|
||||
""" po_items and sub_assembly_items items are both constructed client side without saving.
|
||||
|
||||
Attempt to fix linkages by using temporary names to map final row names.
|
||||
"""
|
||||
new_name_map = {d.temporary_name: d.name for d in self.po_items if d.temporary_name}
|
||||
actual_names = {d.name for d in self.po_items}
|
||||
|
||||
for sub_assy in self.sub_assembly_items:
|
||||
if sub_assy.production_plan_item not in actual_names:
|
||||
sub_assy.production_plan_item = new_name_map.get(sub_assy.production_plan_item)
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_open_sales_orders(self):
|
||||
""" Pull sales orders which are pending to deliver based on criteria selected"""
|
||||
|
@ -667,6 +667,39 @@ class TestProductionPlan(FrappeTestCase):
|
||||
wo_doc.submit()
|
||||
self.assertEqual(wo_doc.qty, 0.55)
|
||||
|
||||
def test_temporary_name_relinking(self):
|
||||
|
||||
pp = frappe.new_doc("Production Plan")
|
||||
|
||||
# this can not be unittested so mocking data that would be expected
|
||||
# from client side.
|
||||
for _ in range(10):
|
||||
po_item = pp.append("po_items", {
|
||||
"name": frappe.generate_hash(length=10),
|
||||
"temporary_name": frappe.generate_hash(length=10),
|
||||
})
|
||||
pp.append("sub_assembly_items", {
|
||||
"production_plan_item": po_item.temporary_name
|
||||
})
|
||||
pp._rename_temporary_references()
|
||||
|
||||
for po_item, subassy_item in zip(pp.po_items, pp.sub_assembly_items):
|
||||
self.assertEqual(po_item.name, subassy_item.production_plan_item)
|
||||
|
||||
# bad links should be erased
|
||||
pp.append("sub_assembly_items", {
|
||||
"production_plan_item": frappe.generate_hash(length=16)
|
||||
})
|
||||
pp._rename_temporary_references()
|
||||
self.assertIsNone(pp.sub_assembly_items[-1].production_plan_item)
|
||||
pp.sub_assembly_items.pop()
|
||||
|
||||
# reattempting on same doc shouldn't change anything
|
||||
pp._rename_temporary_references()
|
||||
for po_item, subassy_item in zip(pp.po_items, pp.sub_assembly_items):
|
||||
self.assertEqual(po_item.name, subassy_item.production_plan_item)
|
||||
|
||||
|
||||
def create_production_plan(**args):
|
||||
"""
|
||||
sales_order (obj): Sales Order Doc Object
|
||||
|
@ -27,7 +27,8 @@
|
||||
"material_request",
|
||||
"material_request_item",
|
||||
"product_bundle_item",
|
||||
"item_reference"
|
||||
"item_reference",
|
||||
"temporary_name"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
@ -204,17 +205,25 @@
|
||||
"fieldtype": "Data",
|
||||
"hidden": 1,
|
||||
"label": "Item Reference"
|
||||
},
|
||||
{
|
||||
"fieldname": "temporary_name",
|
||||
"fieldtype": "Data",
|
||||
"hidden": 1,
|
||||
"label": "temporary name"
|
||||
}
|
||||
],
|
||||
"idx": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2021-06-28 18:31:06.822168",
|
||||
"modified": "2022-03-24 04:54:09.940224",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Manufacturing",
|
||||
"name": "Production Plan Item",
|
||||
"naming_rule": "Random",
|
||||
"owner": "Administrator",
|
||||
"permissions": [],
|
||||
"sort_field": "modified",
|
||||
"sort_order": "ASC"
|
||||
"sort_order": "ASC",
|
||||
"states": []
|
||||
}
|
@ -457,7 +457,8 @@ class WorkOrder(Document):
|
||||
mr_obj.update_requested_qty([self.material_request_item])
|
||||
|
||||
def update_ordered_qty(self):
|
||||
if self.production_plan and self.production_plan_item:
|
||||
if self.production_plan and self.production_plan_item \
|
||||
and not self.production_plan_sub_assembly_item:
|
||||
qty = frappe.get_value("Production Plan Item", self.production_plan_item, "ordered_qty") or 0.0
|
||||
|
||||
if self.docstatus == 1:
|
||||
@ -644,9 +645,13 @@ class WorkOrder(Document):
|
||||
if not self.qty > 0:
|
||||
frappe.throw(_("Quantity to Manufacture must be greater than 0."))
|
||||
|
||||
if self.production_plan and self.production_plan_item:
|
||||
if self.production_plan and self.production_plan_item \
|
||||
and not self.production_plan_sub_assembly_item:
|
||||
qty_dict = frappe.db.get_value("Production Plan Item", self.production_plan_item, ["planned_qty", "ordered_qty"], as_dict=1)
|
||||
|
||||
if not qty_dict:
|
||||
return
|
||||
|
||||
allowance_qty = flt(frappe.db.get_single_value("Manufacturing Settings",
|
||||
"overproduction_percentage_for_work_order"))/100 * qty_dict.get("planned_qty", 0)
|
||||
|
||||
@ -1150,6 +1155,10 @@ def create_job_card(work_order, row, enable_capacity_planning=False, auto_create
|
||||
doc.insert()
|
||||
frappe.msgprint(_("Job card {0} created").format(get_link_to_form("Job Card", doc.name)), alert=True)
|
||||
|
||||
if enable_capacity_planning:
|
||||
# automatically added scheduling rows shouldn't change status to WIP
|
||||
doc.db_set("status", "Open")
|
||||
|
||||
return doc
|
||||
|
||||
def get_work_order_operation_data(work_order, operation, workstation):
|
||||
|
@ -1,7 +1,7 @@
|
||||
[pre_model_sync]
|
||||
erpnext.patches.v12_0.update_is_cancelled_field
|
||||
erpnext.patches.v13_0.add_bin_unique_constraint
|
||||
erpnext.patches.v11_0.rename_production_order_to_work_order
|
||||
erpnext.patches.v13_0.add_bin_unique_constraint
|
||||
erpnext.patches.v11_0.refactor_naming_series
|
||||
erpnext.patches.v11_0.refactor_autoname_naming
|
||||
execute:frappe.reload_doc("accounts", "doctype", "POS Payment Method") #2020-05-28
|
||||
@ -360,4 +360,6 @@ erpnext.patches.v14_0.update_batch_valuation_flag
|
||||
erpnext.patches.v14_0.delete_non_profit_doctypes
|
||||
erpnext.patches.v14_0.update_employee_advance_status
|
||||
erpnext.patches.v13_0.add_cost_center_in_loans
|
||||
erpnext.patches.v13_0.remove_unknown_links_to_prod_plan_items
|
||||
erpnext.patches.v13_0.set_return_against_in_pos_invoice_references
|
||||
erpnext.patches.v13_0.remove_unknown_links_to_prod_plan_items # 24-03-2022
|
||||
erpnext.patches.v13_0.update_expense_claim_status_for_paid_advances
|
||||
|
@ -60,7 +60,7 @@ def execute():
|
||||
|
||||
def convert_to_seconds(value, unit):
|
||||
seconds = 0
|
||||
if value == 0:
|
||||
if not value:
|
||||
return seconds
|
||||
if unit == 'Hours':
|
||||
seconds = value * 3600
|
||||
|
@ -0,0 +1,38 @@
|
||||
import frappe
|
||||
|
||||
|
||||
def execute():
|
||||
'''
|
||||
Fetch and Set is_return & return_against from POS Invoice in POS Invoice References table.
|
||||
'''
|
||||
|
||||
POSClosingEntry = frappe.qb.DocType("POS Closing Entry")
|
||||
open_pos_closing_entries = (
|
||||
frappe.qb
|
||||
.from_(POSClosingEntry)
|
||||
.select(POSClosingEntry.name)
|
||||
.where(POSClosingEntry.docstatus == 0)
|
||||
.run(pluck=True)
|
||||
)
|
||||
|
||||
if not open_pos_closing_entries:
|
||||
return
|
||||
|
||||
POSInvoiceReference = frappe.qb.DocType("POS Invoice Reference")
|
||||
POSInvoice = frappe.qb.DocType("POS Invoice")
|
||||
pos_invoice_references = (
|
||||
frappe.qb
|
||||
.from_(POSInvoiceReference)
|
||||
.join(POSInvoice)
|
||||
.on(POSInvoiceReference.pos_invoice == POSInvoice.name)
|
||||
.select(POSInvoiceReference.name, POSInvoice.is_return, POSInvoice.return_against)
|
||||
.where(POSInvoiceReference.parent.isin(open_pos_closing_entries))
|
||||
.run(as_dict=True)
|
||||
)
|
||||
|
||||
for row in pos_invoice_references:
|
||||
frappe.db.set_value("POS Invoice Reference", row.name, "is_return", row.is_return)
|
||||
if row.is_return:
|
||||
frappe.db.set_value("POS Invoice Reference", row.name, "return_against", row.return_against)
|
||||
else:
|
||||
frappe.db.set_value("POS Invoice Reference", row.name, "return_against", None)
|
@ -0,0 +1,22 @@
|
||||
import frappe
|
||||
|
||||
|
||||
def execute():
|
||||
"""
|
||||
Update Expense Claim status to Paid if:
|
||||
- the entire required amount is already covered via linked advances
|
||||
- the claim is partially paid via advances and the rest is reimbursed
|
||||
"""
|
||||
|
||||
ExpenseClaim = frappe.qb.DocType('Expense Claim')
|
||||
|
||||
(frappe.qb
|
||||
.update(ExpenseClaim)
|
||||
.set(ExpenseClaim.status, 'Paid')
|
||||
.where(
|
||||
((ExpenseClaim.grand_total == 0) | (ExpenseClaim.grand_total == ExpenseClaim.total_amount_reimbursed))
|
||||
& (ExpenseClaim.approval_status == 'Approved')
|
||||
& (ExpenseClaim.docstatus == 1)
|
||||
& (ExpenseClaim.total_sanctioned_amount > 0)
|
||||
)
|
||||
).run()
|
@ -708,6 +708,8 @@ def submit_salary_slips_for_employees(payroll_entry, salary_slips, publish_progr
|
||||
if not_submitted_ss:
|
||||
frappe.msgprint(_("Could not submit some Salary Slips"))
|
||||
|
||||
frappe.flags.via_payroll_entry = False
|
||||
|
||||
@frappe.whitelist()
|
||||
@frappe.validate_and_sanitize_search_inputs
|
||||
def get_payroll_entries_for_jv(doctype, txt, searchfield, start, page_len, filters):
|
||||
|
@ -38,6 +38,8 @@ from erpnext.payroll.doctype.salary_structure.salary_structure import make_salar
|
||||
class TestSalarySlip(unittest.TestCase):
|
||||
def setUp(self):
|
||||
setup_test()
|
||||
frappe.flags.pop("via_payroll_entry", None)
|
||||
|
||||
|
||||
def tearDown(self):
|
||||
frappe.db.rollback()
|
||||
@ -409,15 +411,17 @@ class TestSalarySlip(unittest.TestCase):
|
||||
"email_salary_slip_to_employee": 1
|
||||
})
|
||||
def test_email_salary_slip(self):
|
||||
frappe.db.sql("delete from `tabEmail Queue`")
|
||||
frappe.db.delete("Email Queue")
|
||||
|
||||
make_employee("test_email_salary_slip@salary.com", company="_Test Company")
|
||||
ss = make_employee_salary_slip("test_email_salary_slip@salary.com", "Monthly", "Test Salary Slip Email")
|
||||
user_id = "test_email_salary_slip@salary.com"
|
||||
|
||||
make_employee(user_id, company="_Test Company")
|
||||
ss = make_employee_salary_slip(user_id, "Monthly", "Test Salary Slip Email")
|
||||
ss.company = "_Test Company"
|
||||
ss.save()
|
||||
ss.submit()
|
||||
|
||||
email_queue = frappe.db.sql("""select name from `tabEmail Queue`""")
|
||||
email_queue = frappe.db.a_row_exists("Email Queue")
|
||||
self.assertTrue(email_queue)
|
||||
|
||||
def test_loan_repayment_salary_slip(self):
|
||||
|
@ -1042,9 +1042,9 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
|
||||
var me = this;
|
||||
this.set_dynamic_labels();
|
||||
var company_currency = this.get_company_currency();
|
||||
// Added `ignore_pricing_rule` to determine if document is loading after mapping from another doc
|
||||
// Added `ignore_price_list` to determine if document is loading after mapping from another doc
|
||||
if(this.frm.doc.currency && this.frm.doc.currency !== company_currency
|
||||
&& !this.frm.doc.ignore_pricing_rule) {
|
||||
&& !(this.frm.doc.__onload && this.frm.doc.__onload.ignore_price_list)) {
|
||||
|
||||
this.get_exchange_rate(transaction_date, this.frm.doc.currency, company_currency,
|
||||
function(exchange_rate) {
|
||||
@ -1070,7 +1070,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
|
||||
}
|
||||
|
||||
if(flt(this.frm.doc.conversion_rate)>0.0) {
|
||||
if(this.frm.doc.ignore_pricing_rule) {
|
||||
if(this.frm.doc.__onload && this.frm.doc.__onload.ignore_price_list) {
|
||||
this.calculate_taxes_and_totals();
|
||||
} else if (!this.in_apply_price_list){
|
||||
this.apply_price_list();
|
||||
@ -1144,8 +1144,9 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
|
||||
this.set_dynamic_labels();
|
||||
|
||||
var company_currency = this.get_company_currency();
|
||||
// Added `ignore_pricing_rule` to determine if document is loading after mapping from another doc
|
||||
if(this.frm.doc.price_list_currency !== company_currency && !this.frm.doc.ignore_pricing_rule) {
|
||||
// Added `ignore_price_list` to determine if document is loading after mapping from another doc
|
||||
if(this.frm.doc.price_list_currency !== company_currency &&
|
||||
!(this.frm.doc.__onload && this.frm.doc.__onload.ignore_price_list)) {
|
||||
this.get_exchange_rate(this.frm.doc.posting_date, this.frm.doc.price_list_currency, company_currency,
|
||||
function(exchange_rate) {
|
||||
me.frm.set_value("plc_conversion_rate", exchange_rate);
|
||||
@ -1884,6 +1885,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
|
||||
callback: function(r) {
|
||||
if(!r.exc) {
|
||||
item.item_tax_rate = r.message;
|
||||
me.add_taxes_from_item_tax_template(item.item_tax_rate);
|
||||
me.calculate_taxes_and_totals();
|
||||
}
|
||||
}
|
||||
|
@ -608,6 +608,11 @@ function check_can_calculate_pending_qty(me) {
|
||||
&& doc.fg_completed_qty
|
||||
&& erpnext.stock.bom
|
||||
&& erpnext.stock.bom.name === doc.bom_no;
|
||||
const itemChecks = !!item && !item.allow_alternative_item;
|
||||
const itemChecks = !!item
|
||||
&& !item.allow_alternative_item
|
||||
&& erpnext.stock.bom && erpnext.stock.items
|
||||
&& (item.item_code in erpnext.stock.bom.items);
|
||||
return docChecks && itemChecks;
|
||||
}
|
||||
|
||||
//# sourceURL=serial_no_batch_selector.js
|
||||
|
@ -264,6 +264,15 @@ body.product-page {
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.filter-lookup-input {
|
||||
background-color: white;
|
||||
border: 1px solid var(--gray-300);
|
||||
|
||||
&:focus {
|
||||
border: 1px solid var(--primary);
|
||||
}
|
||||
}
|
||||
|
||||
.filter-label {
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
|
@ -19,8 +19,9 @@ PAN_NUMBER_FORMAT = re.compile("[A-Z]{5}[0-9]{4}[A-Z]{1}")
|
||||
|
||||
|
||||
def validate_gstin_for_india(doc, method):
|
||||
if hasattr(doc, 'gst_state') and doc.gst_state:
|
||||
doc.gst_state_number = state_numbers[doc.gst_state]
|
||||
if hasattr(doc, 'gst_state'):
|
||||
set_gst_state_and_state_number(doc)
|
||||
|
||||
if not hasattr(doc, 'gstin') or not doc.gstin:
|
||||
return
|
||||
|
||||
@ -50,7 +51,6 @@ def validate_gstin_for_india(doc, method):
|
||||
frappe.throw(_("The input you've entered doesn't match the format of GSTIN."), title=_("Invalid GSTIN"))
|
||||
|
||||
validate_gstin_check_digit(doc.gstin)
|
||||
set_gst_state_and_state_number(doc)
|
||||
|
||||
if not doc.gst_state:
|
||||
frappe.throw(_("Please enter GST state"), title=_("Invalid State"))
|
||||
@ -82,17 +82,14 @@ def update_gst_category(doc, method):
|
||||
frappe.db.set_value(link.link_doctype, {'name': link.link_name, 'gst_category': 'Unregistered'}, 'gst_category', 'Registered Regular')
|
||||
|
||||
def set_gst_state_and_state_number(doc):
|
||||
if not doc.gst_state:
|
||||
if not doc.state:
|
||||
return
|
||||
if not doc.gst_state and doc.state:
|
||||
state = doc.state.lower()
|
||||
states_lowercase = {s.lower():s for s in states}
|
||||
if state in states_lowercase:
|
||||
doc.gst_state = states_lowercase[state]
|
||||
else:
|
||||
return
|
||||
|
||||
doc.gst_state_number = state_numbers[doc.gst_state]
|
||||
doc.gst_state_number = state_numbers.get(doc.gst_state)
|
||||
|
||||
def validate_gstin_check_digit(gstin, label='GSTIN'):
|
||||
''' Function to validate the check digit of the GSTIN.'''
|
||||
|
@ -206,6 +206,7 @@ def _make_sales_order(source_name, target_doc=None, ignore_permissions=False):
|
||||
}, target_doc, set_missing_values, ignore_permissions=ignore_permissions)
|
||||
|
||||
# postprocess: fetch shipping address, set missing values
|
||||
doclist.set_onload('ignore_price_list', True)
|
||||
|
||||
return doclist
|
||||
|
||||
@ -269,6 +270,8 @@ def _make_sales_invoice(source_name, target_doc=None, ignore_permissions=False):
|
||||
}
|
||||
}, target_doc, set_missing_values, ignore_permissions=ignore_permissions)
|
||||
|
||||
doclist.set_onload('ignore_price_list', True)
|
||||
|
||||
return doclist
|
||||
|
||||
def _make_customer(source_name, ignore_permissions=False):
|
||||
|
@ -584,6 +584,8 @@ def make_delivery_note(source_name, target_doc=None, skip_item_mapping=False):
|
||||
|
||||
target_doc = get_mapped_doc("Sales Order", source_name, mapper, target_doc, set_missing_values)
|
||||
|
||||
target_doc.set_onload('ignore_price_list', True)
|
||||
|
||||
return target_doc
|
||||
|
||||
@frappe.whitelist()
|
||||
@ -664,6 +666,8 @@ def make_sales_invoice(source_name, target_doc=None, ignore_permissions=False):
|
||||
if automatically_fetch_payment_terms:
|
||||
doclist.set_payment_schedule()
|
||||
|
||||
doclist.set_onload('ignore_price_list', True)
|
||||
|
||||
return doclist
|
||||
|
||||
@frappe.whitelist()
|
||||
|
@ -8,7 +8,7 @@ import frappe
|
||||
from frappe.utils.nestedset import get_root_of
|
||||
|
||||
from erpnext.accounts.doctype.pos_invoice.pos_invoice import get_stock_availability
|
||||
from erpnext.accounts.doctype.pos_profile.pos_profile import get_item_groups
|
||||
from erpnext.accounts.doctype.pos_profile.pos_profile import get_child_nodes, get_item_groups
|
||||
|
||||
|
||||
def search_by_term(search_term, warehouse, price_list):
|
||||
@ -275,3 +275,16 @@ def set_customer_info(fieldname, customer, value=""):
|
||||
contact_doc.set('phone_nos', [{ 'phone': value, 'is_primary_mobile_no': 1}])
|
||||
frappe.db.set_value('Customer', customer, 'mobile_no', value)
|
||||
contact_doc.save()
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_pos_profile_data(pos_profile):
|
||||
pos_profile = frappe.get_doc('POS Profile', pos_profile)
|
||||
pos_profile = pos_profile.as_dict()
|
||||
|
||||
_customer_groups_with_children = []
|
||||
for row in pos_profile.customer_groups:
|
||||
children = get_child_nodes('Customer Group', row.customer_group)
|
||||
_customer_groups_with_children.extend(children)
|
||||
|
||||
pos_profile.customer_groups = _customer_groups_with_children
|
||||
return pos_profile
|
@ -119,10 +119,15 @@ erpnext.PointOfSale.Controller = class {
|
||||
this.allow_negative_stock = flt(message.allow_negative_stock) || false;
|
||||
});
|
||||
|
||||
frappe.db.get_doc("POS Profile", this.pos_profile).then((profile) => {
|
||||
Object.assign(this.settings, profile);
|
||||
this.settings.customer_groups = profile.customer_groups.map(group => group.customer_group);
|
||||
this.make_app();
|
||||
frappe.call({
|
||||
method: "erpnext.selling.page.point_of_sale.point_of_sale.get_pos_profile_data",
|
||||
args: { "pos_profile": this.pos_profile },
|
||||
callback: (res) => {
|
||||
const profile = res.message;
|
||||
Object.assign(this.settings, profile);
|
||||
this.settings.customer_groups = profile.customer_groups.map(group => group.name);
|
||||
this.make_app();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@ -555,7 +560,7 @@ erpnext.PointOfSale.Controller = class {
|
||||
if (this.item_details.$component.is(':visible'))
|
||||
this.edit_item_details_of(item_row);
|
||||
|
||||
if (this.check_serial_batch_selection_needed(item_row))
|
||||
if (this.check_serial_batch_selection_needed(item_row) && !this.item_details.$component.is(':visible'))
|
||||
this.edit_item_details_of(item_row);
|
||||
}
|
||||
|
||||
@ -704,7 +709,7 @@ erpnext.PointOfSale.Controller = class {
|
||||
frappe.dom.freeze();
|
||||
const { doctype, name, current_item } = this.item_details;
|
||||
|
||||
frappe.model.set_value(doctype, name, 'qty', 0)
|
||||
return frappe.model.set_value(doctype, name, 'qty', 0)
|
||||
.then(() => {
|
||||
frappe.model.clear_doc(doctype, name);
|
||||
this.update_cart_html(current_item, true);
|
||||
@ -715,7 +720,14 @@ erpnext.PointOfSale.Controller = class {
|
||||
}
|
||||
|
||||
async save_and_checkout() {
|
||||
this.frm.is_dirty() && await this.frm.save();
|
||||
this.payment.checkout();
|
||||
if (this.frm.is_dirty()) {
|
||||
// only move to payment section if save is successful
|
||||
frappe.route_hooks.after_save = () => this.payment.checkout();
|
||||
return this.frm.save(
|
||||
null, null, null, () => this.cart.toggle_checkout_btn(true) // show checkout button on error
|
||||
);
|
||||
} else {
|
||||
this.payment.checkout();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
@ -60,12 +60,18 @@ erpnext.PointOfSale.ItemDetails = class {
|
||||
return item && item.name == this.current_item.name;
|
||||
}
|
||||
|
||||
toggle_item_details_section(item) {
|
||||
async toggle_item_details_section(item) {
|
||||
const current_item_changed = !this.compare_with_current_item(item);
|
||||
|
||||
// if item is null or highlighted cart item is clicked twice
|
||||
const hide_item_details = !Boolean(item) || !current_item_changed;
|
||||
|
||||
if ((!hide_item_details && current_item_changed) || hide_item_details) {
|
||||
// if item details is being closed OR if item details is opened but item is changed
|
||||
// in both cases, if the current item is a serialized item, then validate and remove the item
|
||||
await this.validate_serial_batch_item();
|
||||
}
|
||||
|
||||
this.events.toggle_item_selector(!hide_item_details);
|
||||
this.toggle_component(!hide_item_details);
|
||||
|
||||
@ -83,7 +89,6 @@ erpnext.PointOfSale.ItemDetails = class {
|
||||
this.render_form(item);
|
||||
this.events.highlight_cart_item(item);
|
||||
} else {
|
||||
this.validate_serial_batch_item();
|
||||
this.current_item = {};
|
||||
}
|
||||
}
|
||||
@ -103,11 +108,11 @@ erpnext.PointOfSale.ItemDetails = class {
|
||||
(serialized && batched && (no_batch_selected || no_serial_selected))) {
|
||||
|
||||
frappe.show_alert({
|
||||
message: __("Item will be removed since no serial / batch no selected."),
|
||||
message: __("Item is removed since no serial / batch no selected."),
|
||||
indicator: 'orange'
|
||||
});
|
||||
frappe.utils.play_sound("cancel");
|
||||
this.events.remove_item_from_cart();
|
||||
return this.events.remove_item_from_cart();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -170,20 +170,24 @@ erpnext.PointOfSale.Payment = class {
|
||||
});
|
||||
|
||||
frappe.ui.form.on('POS Invoice', 'coupon_code', (frm) => {
|
||||
if (!frm.doc.ignore_pricing_rule && frm.doc.coupon_code) {
|
||||
frappe.run_serially([
|
||||
() => frm.doc.ignore_pricing_rule=1,
|
||||
() => frm.trigger('ignore_pricing_rule'),
|
||||
() => frm.doc.ignore_pricing_rule=0,
|
||||
() => frm.trigger('apply_pricing_rule'),
|
||||
() => frm.save(),
|
||||
() => this.update_totals_section(frm.doc)
|
||||
]);
|
||||
} else if (frm.doc.ignore_pricing_rule && frm.doc.coupon_code) {
|
||||
frappe.show_alert({
|
||||
message: __("Ignore Pricing Rule is enabled. Cannot apply coupon code."),
|
||||
indicator: "orange"
|
||||
});
|
||||
if (frm.doc.coupon_code && !frm.applying_pos_coupon_code) {
|
||||
if (!frm.doc.ignore_pricing_rule) {
|
||||
frm.applying_pos_coupon_code = true;
|
||||
frappe.run_serially([
|
||||
() => frm.doc.ignore_pricing_rule=1,
|
||||
() => frm.trigger('ignore_pricing_rule'),
|
||||
() => frm.doc.ignore_pricing_rule=0,
|
||||
() => frm.trigger('apply_pricing_rule'),
|
||||
() => frm.save(),
|
||||
() => this.update_totals_section(frm.doc),
|
||||
() => (frm.applying_pos_coupon_code = false)
|
||||
]);
|
||||
} else if (frm.doc.ignore_pricing_rule) {
|
||||
frappe.show_alert({
|
||||
message: __("Ignore Pricing Rule is enabled. Cannot apply coupon code."),
|
||||
indicator: "orange"
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -4,8 +4,12 @@
|
||||
frappe.ui.form.on('Sales Person', {
|
||||
refresh: function(frm) {
|
||||
if(frm.doc.__onload && frm.doc.__onload.dashboard_info) {
|
||||
var info = frm.doc.__onload.dashboard_info;
|
||||
frm.dashboard.add_indicator(__('Total Contribution Amount: {0}', [format_currency(info.allocated_amount, info.currency)]), 'blue');
|
||||
let info = frm.doc.__onload.dashboard_info;
|
||||
frm.dashboard.add_indicator(__('Total Contribution Amount Against Orders: {0}',
|
||||
[format_currency(info.allocated_amount_against_order, info.currency)]), 'blue');
|
||||
|
||||
frm.dashboard.add_indicator(__('Total Contribution Amount Against Invoices: {0}',
|
||||
[format_currency(info.allocated_amount_against_invoice, info.currency)]), 'blue');
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -28,14 +28,17 @@ class SalesPerson(NestedSet):
|
||||
def load_dashboard_info(self):
|
||||
company_default_currency = get_default_currency()
|
||||
|
||||
allocated_amount = frappe.db.sql("""
|
||||
select sum(allocated_amount)
|
||||
from `tabSales Team`
|
||||
where sales_person = %s and docstatus=1 and parenttype = 'Sales Order'
|
||||
""",(self.sales_person_name))
|
||||
allocated_amount_against_order = flt(frappe.db.get_value('Sales Team',
|
||||
{'docstatus': 1, 'parenttype': 'Sales Order', 'sales_person': self.sales_person_name},
|
||||
'sum(allocated_amount)'))
|
||||
|
||||
allocated_amount_against_invoice = flt(frappe.db.get_value('Sales Team',
|
||||
{'docstatus': 1, 'parenttype': 'Sales Invoice', 'sales_person': self.sales_person_name},
|
||||
'sum(allocated_amount)'))
|
||||
|
||||
info = {}
|
||||
info["allocated_amount"] = flt(allocated_amount[0][0]) if allocated_amount else 0
|
||||
info["allocated_amount_against_order"] = allocated_amount_against_order
|
||||
info["allocated_amount_against_invoice"] = allocated_amount_against_invoice
|
||||
info["currency"] = company_default_currency
|
||||
|
||||
self.set_onload('dashboard_info', info)
|
||||
|
@ -21,9 +21,7 @@ default_mail_footer = """<div style="padding: 7px; text-align: right; color: #88
|
||||
def after_install():
|
||||
frappe.get_doc({'doctype': "Role", "role_name": "Analytics"}).insert()
|
||||
set_single_defaults()
|
||||
create_compact_item_print_custom_field()
|
||||
create_print_uom_after_qty_custom_field()
|
||||
create_print_zero_amount_taxes_custom_field()
|
||||
create_print_setting_custom_fields()
|
||||
add_all_roles_to("Administrator")
|
||||
create_default_cash_flow_mapper_templates()
|
||||
create_default_success_action()
|
||||
@ -77,7 +75,7 @@ def setup_currency_exchange():
|
||||
except frappe.ValidationError:
|
||||
pass
|
||||
|
||||
def create_compact_item_print_custom_field():
|
||||
def create_print_setting_custom_fields():
|
||||
create_custom_field('Print Settings', {
|
||||
'label': _('Compact Item Print'),
|
||||
'fieldname': 'compact_item_print',
|
||||
@ -85,9 +83,6 @@ def create_compact_item_print_custom_field():
|
||||
'default': 1,
|
||||
'insert_after': 'with_letterhead'
|
||||
})
|
||||
|
||||
|
||||
def create_print_uom_after_qty_custom_field():
|
||||
create_custom_field('Print Settings', {
|
||||
'label': _('Print UOM after Quantity'),
|
||||
'fieldname': 'print_uom_after_quantity',
|
||||
@ -95,9 +90,6 @@ def create_print_uom_after_qty_custom_field():
|
||||
'default': 0,
|
||||
'insert_after': 'compact_item_print'
|
||||
})
|
||||
|
||||
|
||||
def create_print_zero_amount_taxes_custom_field():
|
||||
create_custom_field('Print Settings', {
|
||||
'label': _('Print taxes with zero amount'),
|
||||
'fieldname': 'print_taxes_with_zero_amount',
|
||||
|
@ -1,56 +0,0 @@
|
||||
{
|
||||
"add_sample_data": 1,
|
||||
"bank_account": "HDFC",
|
||||
"company_abbr": "FT",
|
||||
"company_name": "For Testing",
|
||||
"company_tagline": "Just for GST",
|
||||
"country": "India",
|
||||
"currency": "INR",
|
||||
"customer_1": "Test Customer 1",
|
||||
"customer_2": "Test Customer 2",
|
||||
"domains": ["Manufacturing"],
|
||||
"email": "great@example.com",
|
||||
"full_name": "Great Tester",
|
||||
"fy_end_date": "2018-03-31",
|
||||
"fy_start_date": "2017-04-01",
|
||||
"is_purchase_item_1": 1,
|
||||
"is_purchase_item_2": 1,
|
||||
"is_purchase_item_3": 0,
|
||||
"is_purchase_item_4": 0,
|
||||
"is_purchase_item_5": 0,
|
||||
"is_sales_item_1": 1,
|
||||
"is_sales_item_2": 1,
|
||||
"is_sales_item_3": 1,
|
||||
"is_sales_item_4": 1,
|
||||
"is_sales_item_5": 1,
|
||||
"item_1": "Test Item 1",
|
||||
"item_2": "Test Item 2",
|
||||
"item_group_1": "Products",
|
||||
"item_group_2": "Products",
|
||||
"item_group_3": "Products",
|
||||
"item_group_4": "Products",
|
||||
"item_group_5": "Products",
|
||||
"item_uom_1": "Unit",
|
||||
"item_uom_2": "Unit",
|
||||
"item_uom_3": "Unit",
|
||||
"item_uom_4": "Unit",
|
||||
"item_uom_5": "Unit",
|
||||
"language": "English (United States)",
|
||||
"password": "test",
|
||||
"setup_website": 1,
|
||||
"supplier_1": "Test Supplier 1",
|
||||
"supplier_2": "Test Supplier 2",
|
||||
"timezone": "Asia/Kolkata",
|
||||
"user_accountant_1": 1,
|
||||
"user_accountant_2": 1,
|
||||
"user_accountant_3": 1,
|
||||
"user_accountant_4": 1,
|
||||
"user_purchaser_1": 1,
|
||||
"user_purchaser_2": 1,
|
||||
"user_purchaser_3": 1,
|
||||
"user_purchaser_4": 1,
|
||||
"user_sales_1": 1,
|
||||
"user_sales_2": 1,
|
||||
"user_sales_3": 1,
|
||||
"user_sales_4": 1
|
||||
}
|
@ -1,179 +0,0 @@
|
||||
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# License: GNU General Public License v3. See license.txt
|
||||
|
||||
|
||||
import json
|
||||
import os
|
||||
import random
|
||||
|
||||
import frappe
|
||||
import frappe.utils
|
||||
from frappe import _
|
||||
from frappe.utils.make_random import add_random_children
|
||||
|
||||
|
||||
def make_sample_data(domains, make_dependent = False):
|
||||
"""Create a few opportunities, quotes, material requests, issues, todos, projects
|
||||
to help the user get started"""
|
||||
|
||||
if make_dependent:
|
||||
items = frappe.get_all("Item", {'is_sales_item': 1})
|
||||
customers = frappe.get_all("Customer")
|
||||
warehouses = frappe.get_all("Warehouse")
|
||||
|
||||
if items and customers:
|
||||
for i in range(3):
|
||||
customer = random.choice(customers).name
|
||||
make_opportunity(items, customer)
|
||||
make_quote(items, customer)
|
||||
|
||||
if items and warehouses:
|
||||
make_material_request(frappe.get_all("Item"))
|
||||
|
||||
make_projects(domains)
|
||||
import_notification()
|
||||
|
||||
def make_opportunity(items, customer):
|
||||
b = frappe.get_doc({
|
||||
"doctype": "Opportunity",
|
||||
"opportunity_from": "Customer",
|
||||
"customer": customer,
|
||||
"opportunity_type": _("Sales"),
|
||||
"with_items": 1
|
||||
})
|
||||
|
||||
add_random_children(b, "items", rows=len(items), randomize = {
|
||||
"qty": (1, 5),
|
||||
"item_code": ["Item"]
|
||||
}, unique="item_code")
|
||||
|
||||
b.insert(ignore_permissions=True)
|
||||
|
||||
b.add_comment('Comment', text="This is a dummy record")
|
||||
|
||||
def make_quote(items, customer):
|
||||
qtn = frappe.get_doc({
|
||||
"doctype": "Quotation",
|
||||
"quotation_to": "Customer",
|
||||
"party_name": customer,
|
||||
"order_type": "Sales"
|
||||
})
|
||||
|
||||
add_random_children(qtn, "items", rows=len(items), randomize = {
|
||||
"qty": (1, 5),
|
||||
"item_code": ["Item"]
|
||||
}, unique="item_code")
|
||||
|
||||
qtn.insert(ignore_permissions=True)
|
||||
|
||||
qtn.add_comment('Comment', text="This is a dummy record")
|
||||
|
||||
def make_material_request(items):
|
||||
for i in items:
|
||||
mr = frappe.get_doc({
|
||||
"doctype": "Material Request",
|
||||
"material_request_type": "Purchase",
|
||||
"schedule_date": frappe.utils.add_days(frappe.utils.nowdate(), 7),
|
||||
"items": [{
|
||||
"schedule_date": frappe.utils.add_days(frappe.utils.nowdate(), 7),
|
||||
"item_code": i.name,
|
||||
"qty": 10
|
||||
}]
|
||||
})
|
||||
mr.insert()
|
||||
mr.submit()
|
||||
|
||||
mr.add_comment('Comment', text="This is a dummy record")
|
||||
|
||||
|
||||
def make_issue():
|
||||
pass
|
||||
|
||||
def make_projects(domains):
|
||||
current_date = frappe.utils.nowdate()
|
||||
project = frappe.get_doc({
|
||||
"doctype": "Project",
|
||||
"project_name": "ERPNext Implementation",
|
||||
})
|
||||
|
||||
tasks = [
|
||||
{
|
||||
"title": "Explore ERPNext",
|
||||
"start_date": current_date,
|
||||
"end_date": current_date,
|
||||
"file": "explore.md"
|
||||
}]
|
||||
|
||||
if 'Education' in domains:
|
||||
tasks += [
|
||||
{
|
||||
"title": _("Setup your Institute in ERPNext"),
|
||||
"start_date": current_date,
|
||||
"end_date": frappe.utils.add_days(current_date, 1),
|
||||
"file": "education_masters.md"
|
||||
},
|
||||
{
|
||||
"title": "Setup Master Data",
|
||||
"start_date": current_date,
|
||||
"end_date": frappe.utils.add_days(current_date, 1),
|
||||
"file": "education_masters.md"
|
||||
}]
|
||||
|
||||
else:
|
||||
tasks += [
|
||||
{
|
||||
"title": "Setup Your Company",
|
||||
"start_date": current_date,
|
||||
"end_date": frappe.utils.add_days(current_date, 1),
|
||||
"file": "masters.md"
|
||||
},
|
||||
{
|
||||
"title": "Start Tracking your Sales",
|
||||
"start_date": current_date,
|
||||
"end_date": frappe.utils.add_days(current_date, 2),
|
||||
"file": "sales.md"
|
||||
},
|
||||
{
|
||||
"title": "Start Managing Purchases",
|
||||
"start_date": current_date,
|
||||
"end_date": frappe.utils.add_days(current_date, 3),
|
||||
"file": "purchase.md"
|
||||
},
|
||||
{
|
||||
"title": "Import Data",
|
||||
"start_date": current_date,
|
||||
"end_date": frappe.utils.add_days(current_date, 4),
|
||||
"file": "import_data.md"
|
||||
},
|
||||
{
|
||||
"title": "Go Live!",
|
||||
"start_date": current_date,
|
||||
"end_date": frappe.utils.add_days(current_date, 5),
|
||||
"file": "go_live.md"
|
||||
}]
|
||||
|
||||
for t in tasks:
|
||||
with open (os.path.join(os.path.dirname(__file__), "tasks", t['file'])) as f:
|
||||
t['description'] = frappe.utils.md_to_html(f.read())
|
||||
del t['file']
|
||||
|
||||
project.append('tasks', t)
|
||||
|
||||
project.insert(ignore_permissions=True)
|
||||
|
||||
def import_notification():
|
||||
'''Import notification for task start'''
|
||||
with open (os.path.join(os.path.dirname(__file__), "tasks/task_alert.json")) as f:
|
||||
notification = frappe.get_doc(json.loads(f.read())[0])
|
||||
notification.insert()
|
||||
|
||||
# trigger the first message!
|
||||
from frappe.email.doctype.notification.notification import trigger_daily_alerts
|
||||
trigger_daily_alerts()
|
||||
|
||||
def test_sample():
|
||||
frappe.db.sql('delete from `tabNotification`')
|
||||
frappe.db.sql('delete from tabProject')
|
||||
frappe.db.sql('delete from tabTask')
|
||||
make_projects('Education')
|
||||
import_notification()
|
@ -7,7 +7,6 @@ from frappe import _
|
||||
|
||||
from .operations import company_setup
|
||||
from .operations import install_fixtures as fixtures
|
||||
from .operations import sample_data
|
||||
|
||||
|
||||
def get_setup_stages(args=None):
|
||||
@ -103,16 +102,6 @@ def fin(args):
|
||||
frappe.local.message_log = []
|
||||
login_as_first_user(args)
|
||||
|
||||
make_sample_data(args.get('domains'))
|
||||
|
||||
def make_sample_data(domains):
|
||||
try:
|
||||
sample_data.make_sample_data(domains)
|
||||
except Exception:
|
||||
# clear message
|
||||
if frappe.message_log:
|
||||
frappe.message_log.pop()
|
||||
pass
|
||||
|
||||
def login_as_first_user(args):
|
||||
if args.get("email") and hasattr(frappe.local, "login_manager"):
|
||||
|
@ -1,9 +0,0 @@
|
||||
Lets start making things in ERPNext that are representative of your institution.
|
||||
|
||||
1. Make a list of **Programs** that you offer
|
||||
1. Add a few **Courses** that your programs cover
|
||||
1. Create **Academic Terms** and **Academic Years**
|
||||
1. Start adding **Students**
|
||||
1. Group your students into **Batches**
|
||||
|
||||
Watch this video to learn more about ERPNext Education: https://www.youtube.com/watch?v=f6foQOyGzdA
|
@ -1,7 +0,0 @@
|
||||
Thanks for checking this out! ❤️
|
||||
|
||||
If you are evaluating an ERP system for the first time, this is going to be quite a task! But don't worry, ERPNext is awesome.
|
||||
|
||||
First, get familiar with the surroundings. ERPNext covers a *lot of features*, go to the home page and click on the "Explore" icon.
|
||||
|
||||
All the best!
|
@ -1,18 +0,0 @@
|
||||
Ready to go live with ERPNext? 🏁🏁🏁
|
||||
|
||||
Here are the steps:
|
||||
|
||||
1. Sync up your **Chart of Accounts**
|
||||
3. Add your opening stock using **Stock Reconciliation**
|
||||
4. Add your open invoices (both sales and purchase)
|
||||
3. Add your opening account balances by making a **Journal Entry**
|
||||
|
||||
If you need help for going live, sign up for an account at erpnext.com or find a partner to help you with this.
|
||||
|
||||
Or you can watch these videos 📺:
|
||||
|
||||
Setup your chart of accounts: https://www.youtube.com/watch?v=AcfMCT7wLLo
|
||||
|
||||
Add Open Stock: https://www.youtube.com/watch?v=nlHX0ZZ84Lw
|
||||
|
||||
Add Opening Balances: https://www.youtube.com/watch?v=nlHX0ZZ84Lw
|
@ -1,5 +0,0 @@
|
||||
Lets import some data! 💪💪
|
||||
|
||||
If you are already running a business, you most likely have your Items, Customers or Suppliers in some spreadsheet file somewhere, import it into ERPNext with the Data Import Tool.
|
||||
|
||||
Watch this video to get started: https://www.youtube.com/watch?v=Ta2Xx3QoK3E
|
@ -1,7 +0,0 @@
|
||||
Start building a model of your business in ERPNext by adding your Items and Customers.
|
||||
|
||||
These videos 📺 will help you get started:
|
||||
|
||||
Adding Customers and Suppliers: https://www.youtube.com/watch?v=zsrrVDk6VBs
|
||||
|
||||
Adding Items and Prices: https://www.youtube.com/watch?v=FcOsV-e8ymE
|
@ -1,10 +0,0 @@
|
||||
How to manage your purchasing in ERPNext 🛒🛒🛒:
|
||||
|
||||
1. Add a few **Suppliers**
|
||||
2. Find out what you need by making **Material Requests**.
|
||||
3. Now start placing orders via **Purchase Order**.
|
||||
4. When your suppliers deliver, make **Purchase Receipts**
|
||||
|
||||
Now never run out of stock again! 😎
|
||||
|
||||
Watch this video 📺 to get an overview: https://www.youtube.com/watch?v=4TN9kPyfIqM
|
@ -1,8 +0,0 @@
|
||||
Start managing your sales with ERPNext 🔔🔔🔔:
|
||||
|
||||
1. Add potential business contacts as **Leads**
|
||||
2. Udpate your deals in pipeline in **Opportunities**
|
||||
3. Send proposals to your leads or customers with **Quotations**
|
||||
4. Track confirmed orders with **Sales Orders**
|
||||
|
||||
Watch this video 📺 to get an overview: https://www.youtube.com/watch?v=o9XCSZHJfpA
|
@ -1,5 +0,0 @@
|
||||
Lets import some data! 💪💪
|
||||
|
||||
If you are already running a Institute, you most likely have your Students in some spreadsheet file somewhere. Import it into ERPNext with the Data Import Tool.
|
||||
|
||||
Watch this video to get started: https://www.youtube.com/watch?v=Ta2Xx3QoK3E
|
@ -1,28 +0,0 @@
|
||||
[
|
||||
{
|
||||
"attach_print": 0,
|
||||
"condition": "doc.status in ('Open', 'Overdue')",
|
||||
"date_changed": "exp_end_date",
|
||||
"days_in_advance": 0,
|
||||
"docstatus": 0,
|
||||
"doctype": "Notification",
|
||||
"document_type": "Task",
|
||||
"enabled": 1,
|
||||
"event": "Days After",
|
||||
"is_standard": 0,
|
||||
"message": "<p>Task due today:</p>\n\n<div>\n{{ doc.description }}\n</div>\n\n<hr>\n<p style=\"font-size: 85%\">\nThis is a notification for a task that is due today, and a sample <b>Notification</b>. In ERPNext you can setup notifications on anything, Invoices, Orders, Leads, Opportunities, so you never miss a thing.\n<br>To edit this, and setup other alerts, just type <b>Notification</b> in the search bar.</p>",
|
||||
"method": null,
|
||||
"modified": "2017-03-09 07:34:58.168370",
|
||||
"module": null,
|
||||
"name": "Task Due Alert",
|
||||
"recipients": [
|
||||
{
|
||||
"cc": null,
|
||||
"condition": null,
|
||||
"email_by_document_field": "owner"
|
||||
}
|
||||
],
|
||||
"subject": "{{ doc.subject }}",
|
||||
"value_changed": null
|
||||
}
|
||||
]
|
@ -1,12 +0,0 @@
|
||||
import json
|
||||
import os
|
||||
|
||||
from frappe.desk.page.setup_wizard.setup_wizard import setup_complete
|
||||
|
||||
|
||||
def complete():
|
||||
with open(os.path.join(os.path.dirname(__file__),
|
||||
'data', 'test_mfg.json'), 'r') as f:
|
||||
data = json.loads(f.read())
|
||||
|
||||
setup_complete(data)
|
@ -5,28 +5,17 @@
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.utils import add_days, flt, get_datetime_str, nowdate
|
||||
from frappe.utils.data import now_datetime
|
||||
from frappe.utils.nestedset import get_ancestors_of, get_root_of # noqa
|
||||
|
||||
from erpnext import get_default_company
|
||||
|
||||
|
||||
def get_root_of(doctype):
|
||||
"""Get root element of a DocType with a tree structure"""
|
||||
result = frappe.db.sql_list("""select name from `tab%s`
|
||||
where lft=1 and rgt=(select max(rgt) from `tab%s` where docstatus < 2)""" %
|
||||
(doctype, doctype))
|
||||
return result[0] if result else None
|
||||
|
||||
def get_ancestors_of(doctype, name):
|
||||
"""Get ancestor elements of a DocType with a tree structure"""
|
||||
lft, rgt = frappe.db.get_value(doctype, name, ["lft", "rgt"])
|
||||
result = frappe.db.sql_list("""select name from `tab%s`
|
||||
where lft<%s and rgt>%s order by lft desc""" % (doctype, "%s", "%s"), (lft, rgt))
|
||||
return result or []
|
||||
|
||||
def before_tests():
|
||||
frappe.clear_cache()
|
||||
# complete setup if missing
|
||||
from frappe.desk.page.setup_wizard.setup_wizard import setup_complete
|
||||
current_year = now_datetime().year
|
||||
if not frappe.get_list("Company"):
|
||||
setup_complete({
|
||||
"currency" :"USD",
|
||||
@ -36,8 +25,8 @@ def before_tests():
|
||||
"company_abbr" :"WP",
|
||||
"industry" :"Manufacturing",
|
||||
"country" :"United States",
|
||||
"fy_start_date" :"2021-01-01",
|
||||
"fy_end_date" :"2021-12-31",
|
||||
"fy_start_date" :f"{current_year}-01-01",
|
||||
"fy_end_date" :f"{current_year}-12-31",
|
||||
"language" :"english",
|
||||
"company_tagline" :"Testing",
|
||||
"email" :"test@erpnext.com",
|
||||
@ -51,7 +40,6 @@ def before_tests():
|
||||
frappe.db.sql("delete from `tabSalary Slip`")
|
||||
frappe.db.sql("delete from `tabItem Price`")
|
||||
|
||||
frappe.db.set_value("Stock Settings", None, "auto_insert_price_list_rate_if_missing", 0)
|
||||
enable_all_roles_and_domains()
|
||||
set_defaults_for_tests()
|
||||
|
||||
@ -142,13 +130,13 @@ def enable_all_roles_and_domains():
|
||||
add_all_roles_to('Administrator')
|
||||
|
||||
def set_defaults_for_tests():
|
||||
from frappe.utils.nestedset import get_root_of
|
||||
|
||||
selling_settings = frappe.get_single("Selling Settings")
|
||||
selling_settings.customer_group = get_root_of("Customer Group")
|
||||
selling_settings.territory = get_root_of("Territory")
|
||||
selling_settings.save()
|
||||
|
||||
frappe.db.set_single_value("Stock Settings", "auto_insert_price_list_rate_if_missing", 0)
|
||||
|
||||
|
||||
def insert_record(records):
|
||||
for r in records:
|
||||
|
@ -40,18 +40,15 @@ def get_data(item_code=None, warehouse=None, item_group=None,
|
||||
filters=filters,
|
||||
order_by=sort_by + ' ' + sort_order,
|
||||
limit_start=start,
|
||||
limit_page_length='21')
|
||||
limit_page_length=21)
|
||||
|
||||
precision = cint(frappe.db.get_single_value("System Settings", "float_precision"))
|
||||
|
||||
for item in items:
|
||||
item.update({
|
||||
'item_name': frappe.get_cached_value(
|
||||
"Item", item.item_code, 'item_name'),
|
||||
'disable_quick_entry': frappe.get_cached_value(
|
||||
"Item", item.item_code, 'has_batch_no')
|
||||
or frappe.get_cached_value(
|
||||
"Item", item.item_code, 'has_serial_no'),
|
||||
'item_name': frappe.get_cached_value("Item", item.item_code, 'item_name'),
|
||||
'disable_quick_entry': frappe.get_cached_value( "Item", item.item_code, 'has_batch_no')
|
||||
or frappe.get_cached_value( "Item", item.item_code, 'has_serial_no'),
|
||||
'projected_qty': flt(item.projected_qty, precision),
|
||||
'reserved_qty': flt(item.reserved_qty, precision),
|
||||
'reserved_qty_for_production': flt(item.reserved_qty_for_production, precision),
|
||||
|
@ -519,6 +519,8 @@ def make_sales_invoice(source_name, target_doc=None):
|
||||
if automatically_fetch_payment_terms:
|
||||
doc.set_payment_schedule()
|
||||
|
||||
doc.set_onload('ignore_price_list', True)
|
||||
|
||||
return doc
|
||||
|
||||
@frappe.whitelist()
|
||||
|
@ -422,7 +422,6 @@
|
||||
"fieldname": "has_batch_no",
|
||||
"fieldtype": "Check",
|
||||
"label": "Has Batch No",
|
||||
"no_copy": 1,
|
||||
"oldfieldname": "has_batch_no",
|
||||
"oldfieldtype": "Select"
|
||||
},
|
||||
@ -472,7 +471,6 @@
|
||||
"fieldname": "has_serial_no",
|
||||
"fieldtype": "Check",
|
||||
"label": "Has Serial No",
|
||||
"no_copy": 1,
|
||||
"oldfieldname": "has_serial_no",
|
||||
"oldfieldtype": "Select"
|
||||
},
|
||||
@ -921,7 +919,7 @@
|
||||
"image_field": "image",
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2022-02-11 08:07:46.663220",
|
||||
"modified": "2022-03-25 06:38:55.942304",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Stock",
|
||||
"name": "Item",
|
||||
@ -994,4 +992,4 @@
|
||||
"states": [],
|
||||
"title_field": "item_name",
|
||||
"track_changes": 1
|
||||
}
|
||||
}
|
@ -685,6 +685,12 @@ class TestItem(FrappeTestCase):
|
||||
# standalone return
|
||||
make_purchase_receipt(is_return=True, qty=-1, **typical_args)
|
||||
|
||||
def test_item_dashboard(self):
|
||||
from erpnext.stock.dashboard.item_dashboard import get_data
|
||||
|
||||
self.assertTrue(get_data(item_code="_Test Item"))
|
||||
self.assertTrue(get_data(warehouse="_Test Warehouse - _TC"))
|
||||
self.assertTrue(get_data(item_group="All Item Groups"))
|
||||
|
||||
|
||||
def set_item_variant_settings(fields):
|
||||
|
@ -13,10 +13,16 @@ frappe.ui.form.on('Item Variant Settings', {
|
||||
const exclude_field_types = ['HTML', 'Section Break', 'Column Break', 'Button', 'Read Only'];
|
||||
|
||||
frappe.model.with_doctype('Item', () => {
|
||||
const field_label_map = {};
|
||||
frappe.get_meta('Item').fields.forEach(d => {
|
||||
field_label_map[d.fieldname] = __(d.label) + ` (${d.fieldname})`;
|
||||
|
||||
if (!in_list(exclude_field_types, d.fieldtype)
|
||||
&& !d.no_copy && !in_list(exclude_fields, d.fieldname)) {
|
||||
allow_fields.push(d.fieldname);
|
||||
allow_fields.push({
|
||||
label: field_label_map[d.fieldname],
|
||||
value: d.fieldname,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -823,6 +823,7 @@ def make_purchase_invoice(source_name, target_doc=None):
|
||||
}
|
||||
}, target_doc, set_missing_values)
|
||||
|
||||
doclist.set_onload('ignore_price_list', True)
|
||||
return doclist
|
||||
|
||||
def get_invoiced_qty_map(purchase_receipt):
|
||||
|
@ -396,6 +396,21 @@ class TestPutawayRule(FrappeTestCase):
|
||||
rule_1.delete()
|
||||
rule_2.delete()
|
||||
|
||||
def test_warehouse_capacity_dashbord(self):
|
||||
from erpnext.stock.dashboard.warehouse_capacity_dashboard import get_data
|
||||
|
||||
item = "_Rice"
|
||||
rule = create_putaway_rule(item_code=item, warehouse=self.warehouse_1, capacity=500,
|
||||
uom="Kg")
|
||||
|
||||
capacities = get_data(warehouse=self.warehouse_1)
|
||||
for capacity in capacities:
|
||||
if capacity.item_code == item and capacity.warehouse == self.warehouse_1:
|
||||
self.assertEqual(capacity.stock_capacity, 500)
|
||||
|
||||
get_data(warehouse=self.warehouse_1)
|
||||
rule.delete()
|
||||
|
||||
def create_putaway_rule(**args):
|
||||
args = frappe._dict(args)
|
||||
putaway = frappe.new_doc("Putaway Rule")
|
||||
|
@ -3,6 +3,7 @@
|
||||
|
||||
|
||||
import json
|
||||
from typing import List, Optional, Union
|
||||
|
||||
import frappe
|
||||
from frappe import ValidationError, _
|
||||
@ -574,22 +575,30 @@ def get_delivery_note_serial_no(item_code, qty, delivery_note):
|
||||
return serial_nos
|
||||
|
||||
@frappe.whitelist()
|
||||
def auto_fetch_serial_number(qty, item_code, warehouse,
|
||||
posting_date=None, batch_nos=None, for_doctype=None, exclude_sr_nos=None):
|
||||
def auto_fetch_serial_number(
|
||||
qty: float,
|
||||
item_code: str,
|
||||
warehouse: str,
|
||||
posting_date: Optional[str] = None,
|
||||
batch_nos: Optional[Union[str, List[str]]] = None,
|
||||
for_doctype: Optional[str] = None,
|
||||
exclude_sr_nos: Optional[List[str]] = None
|
||||
) -> List[str]:
|
||||
|
||||
filters = frappe._dict({"item_code": item_code, "warehouse": warehouse})
|
||||
|
||||
if exclude_sr_nos is None:
|
||||
exclude_sr_nos = []
|
||||
else:
|
||||
exclude_sr_nos = safe_json_loads(exclude_sr_nos)
|
||||
exclude_sr_nos = get_serial_nos(clean_serial_no_string("\n".join(exclude_sr_nos)))
|
||||
|
||||
if batch_nos:
|
||||
batch_nos = safe_json_loads(batch_nos)
|
||||
if isinstance(batch_nos, list):
|
||||
filters.batch_no = batch_nos
|
||||
elif isinstance(batch_nos, str):
|
||||
filters.batch_no = [batch_nos]
|
||||
else:
|
||||
filters.batch_no = [str(batch_nos)]
|
||||
|
||||
if posting_date:
|
||||
filters.expiry_date = posting_date
|
||||
@ -602,25 +611,60 @@ def auto_fetch_serial_number(qty, item_code, warehouse,
|
||||
|
||||
return sorted([d.get('name') for d in serial_numbers])
|
||||
|
||||
def get_delivered_serial_nos(serial_nos):
|
||||
'''
|
||||
Returns serial numbers that delivered from the list of serial numbers
|
||||
'''
|
||||
from frappe.query_builder.functions import Coalesce
|
||||
|
||||
SerialNo = frappe.qb.DocType("Serial No")
|
||||
serial_nos = get_serial_nos(serial_nos)
|
||||
query = frappe.qb.select(SerialNo.name).from_(SerialNo).where(
|
||||
(SerialNo.name.isin(serial_nos))
|
||||
& (Coalesce(SerialNo.delivery_document_type, "") != "")
|
||||
)
|
||||
|
||||
result = query.run()
|
||||
if result and len(result) > 0:
|
||||
delivered_serial_nos = [row[0] for row in result]
|
||||
return delivered_serial_nos
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_pos_reserved_serial_nos(filters):
|
||||
if isinstance(filters, str):
|
||||
filters = json.loads(filters)
|
||||
|
||||
pos_transacted_sr_nos = frappe.db.sql("""select item.serial_no as serial_no
|
||||
from `tabPOS Invoice` p, `tabPOS Invoice Item` item
|
||||
where p.name = item.parent
|
||||
and p.consolidated_invoice is NULL
|
||||
and p.docstatus = 1
|
||||
and item.docstatus = 1
|
||||
and item.item_code = %(item_code)s
|
||||
and item.warehouse = %(warehouse)s
|
||||
and item.serial_no is NOT NULL and item.serial_no != ''
|
||||
""", filters, as_dict=1)
|
||||
POSInvoice = frappe.qb.DocType("POS Invoice")
|
||||
POSInvoiceItem = frappe.qb.DocType("POS Invoice Item")
|
||||
query = frappe.qb.from_(
|
||||
POSInvoice
|
||||
).from_(
|
||||
POSInvoiceItem
|
||||
).select(
|
||||
POSInvoice.is_return,
|
||||
POSInvoiceItem.serial_no
|
||||
).where(
|
||||
(POSInvoice.name == POSInvoiceItem.parent)
|
||||
& (POSInvoice.docstatus == 1)
|
||||
& (POSInvoiceItem.docstatus == 1)
|
||||
& (POSInvoiceItem.item_code == filters.get('item_code'))
|
||||
& (POSInvoiceItem.warehouse == filters.get('warehouse'))
|
||||
& (POSInvoiceItem.serial_no.isnotnull())
|
||||
& (POSInvoiceItem.serial_no != '')
|
||||
)
|
||||
|
||||
pos_transacted_sr_nos = query.run(as_dict=True)
|
||||
|
||||
reserved_sr_nos = []
|
||||
returned_sr_nos = []
|
||||
for d in pos_transacted_sr_nos:
|
||||
reserved_sr_nos += get_serial_nos(d.serial_no)
|
||||
if d.is_return == 0:
|
||||
reserved_sr_nos += get_serial_nos(d.serial_no)
|
||||
elif d.is_return == 1:
|
||||
returned_sr_nos += get_serial_nos(d.serial_no)
|
||||
|
||||
for sr_no in returned_sr_nos:
|
||||
reserved_sr_nos.remove(sr_no)
|
||||
|
||||
return reserved_sr_nos
|
||||
|
||||
|
@ -274,7 +274,8 @@ class TestSerialNo(FrappeTestCase):
|
||||
msg=f"{partial_fetch} should be subset of {first_fetch}")
|
||||
|
||||
# exclusion
|
||||
remaining = auto_fetch_serial_number(3, item_code, warehouse, exclude_sr_nos=partial_fetch)
|
||||
remaining = auto_fetch_serial_number(3, item_code, warehouse,
|
||||
exclude_sr_nos=json.dumps(partial_fetch))
|
||||
self.assertEqual(sorted(remaining + partial_fetch), first_fetch)
|
||||
|
||||
# batchwise
|
||||
|
@ -1,72 +1,32 @@
|
||||
{
|
||||
"allow_copy": 0,
|
||||
"allow_guest_to_view": 0,
|
||||
"allow_import": 0,
|
||||
"allow_rename": 0,
|
||||
"beta": 0,
|
||||
"creation": "2017-08-29 16:33:33.978574",
|
||||
"custom": 0,
|
||||
"docstatus": 0,
|
||||
"doctype": "DocType",
|
||||
"document_type": "",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"actions": [],
|
||||
"creation": "2022-02-11 11:26:20.611960",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"field_name"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "field_name",
|
||||
"fieldtype": "Select",
|
||||
"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": "Field Name",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"options": "",
|
||||
"permlevel": 0,
|
||||
"precision": "",
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"read_only": 0,
|
||||
"remember_last_selected_value": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 1,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"unique": 0
|
||||
"fieldname": "field_name",
|
||||
"fieldtype": "Autocomplete",
|
||||
"in_list_view": 1,
|
||||
"label": "Field Name",
|
||||
"reqd": 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": 1,
|
||||
"max_attachments": 0,
|
||||
"modified": "2017-08-29 17:19:20.353197",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Stock",
|
||||
"name": "Variant Field",
|
||||
"name_case": "",
|
||||
"owner": "Administrator",
|
||||
"permissions": [],
|
||||
"quick_entry": 1,
|
||||
"read_only": 0,
|
||||
"read_only_onload": 0,
|
||||
"show_name_in_global_search": 0,
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"track_changes": 1,
|
||||
"track_seen": 0
|
||||
],
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2022-03-25 05:48:30.946201",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Stock",
|
||||
"name": "Variant Field",
|
||||
"owner": "Administrator",
|
||||
"permissions": [],
|
||||
"quick_entry": 1,
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
@ -52,24 +52,6 @@
|
||||
|
||||
</div>
|
||||
|
||||
<script>
|
||||
frappe.ready(() => {
|
||||
$('.product-filter-filter').on('keydown', frappe.utils.debounce((e) => {
|
||||
const $input = $(e.target);
|
||||
const keyword = ($input.val() || '').toLowerCase();
|
||||
const $filter_options = $input.next('.filter-options');
|
||||
|
||||
$filter_options.find('.custom-control').show();
|
||||
$filter_options.find('.custom-control').each((i, el) => {
|
||||
const $el = $(el);
|
||||
const value = $el.data('value').toLowerCase();
|
||||
if (!value.includes(keyword)) {
|
||||
$el.hide();
|
||||
}
|
||||
});
|
||||
}, 300));
|
||||
})
|
||||
</script>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -300,13 +300,13 @@
|
||||
|
||||
{% if values | len > 20 %}
|
||||
<!-- show inline filter if values more than 20 -->
|
||||
<input type="text" class="form-control form-control-sm mb-2 product-filter-filter"/>
|
||||
<input type="text" class="form-control form-control-sm mb-2 filter-lookup-input" placeholder="Search {{ item_field.label + 's' }}"/>
|
||||
{% endif %}
|
||||
|
||||
{% if values %}
|
||||
<div class="filter-options">
|
||||
{% for value in values %}
|
||||
<div class="checkbox" data-value="{{ value }}">
|
||||
<div class="filter-lookup-wrapper checkbox" data-value="{{ value }}">
|
||||
<label for="{{value}}">
|
||||
<input type="checkbox"
|
||||
class="product-filter field-filter"
|
||||
@ -329,16 +329,16 @@
|
||||
{%- macro attribute_filter_section(filters)-%}
|
||||
{% for attribute in filters %}
|
||||
<div class="mb-4 filter-block pb-5">
|
||||
<div class="filter-label mb-3">{{ attribute.name}}</div>
|
||||
{% if values | len > 20 %}
|
||||
<div class="filter-label mb-3">{{ attribute.name }}</div>
|
||||
{% if attribute.item_attribute_values | len > 20 %}
|
||||
<!-- show inline filter if values more than 20 -->
|
||||
<input type="text" class="form-control form-control-sm mb-2 product-filter-filter"/>
|
||||
<input type="text" class="form-control form-control-sm mb-2 filter-lookup-input" placeholder="Search {{ attribute.name + 's' }}"/>
|
||||
{% endif %}
|
||||
|
||||
{% if attribute.item_attribute_values %}
|
||||
<div class="filter-options">
|
||||
{% for attr_value in attribute.item_attribute_values %}
|
||||
<div class="checkbox">
|
||||
<div class="filter-lookup-wrapper checkbox" data-value="{{ attr_value }}">
|
||||
<label data-value="{{ attr_value }}">
|
||||
<input type="checkbox"
|
||||
class="product-filter attribute-filter"
|
||||
|
@ -1,13 +1,14 @@
|
||||
""" dumb test to check all function calls on known form loads """
|
||||
|
||||
import unittest
|
||||
""" smoak tests to check basic functionality calls on known form loads."""
|
||||
|
||||
import frappe
|
||||
from frappe.desk.form.load import getdoc
|
||||
from frappe.tests.utils import FrappeTestCase, change_settings
|
||||
from frappe.www.printview import get_html_and_style
|
||||
|
||||
|
||||
class TestFormLoads(unittest.TestCase):
|
||||
class TestFormLoads(FrappeTestCase):
|
||||
|
||||
@change_settings("Print Settings", {"allow_print_for_cancelled": 1})
|
||||
def test_load(self):
|
||||
erpnext_modules = frappe.get_all("Module Def", filters={"app_name": "erpnext"}, pluck="name")
|
||||
doctypes = frappe.get_all("DocType", {"istable": 0, "issingle": 0, "is_virtual": 0, "module": ("in", erpnext_modules)}, pluck="name")
|
||||
@ -17,14 +18,35 @@ class TestFormLoads(unittest.TestCase):
|
||||
if not last_doc:
|
||||
continue
|
||||
with self.subTest(msg=f"Loading {doctype} - {last_doc}", doctype=doctype, last_doc=last_doc):
|
||||
try:
|
||||
# reset previous response
|
||||
frappe.response = frappe._dict({"docs":[]})
|
||||
frappe.response.docinfo = None
|
||||
self.assertFormLoad(doctype, last_doc)
|
||||
self.assertDocPrint(doctype, last_doc)
|
||||
|
||||
getdoc(doctype, last_doc)
|
||||
except Exception as e:
|
||||
self.fail(f"Failed to load {doctype} - {last_doc}: {e}")
|
||||
def assertFormLoad(self, doctype, docname):
|
||||
# reset previous response
|
||||
frappe.response = frappe._dict({"docs":[]})
|
||||
frappe.response.docinfo = None
|
||||
|
||||
self.assertTrue(frappe.response.docs, msg=f"expected document in reponse, found: {frappe.response.docs}")
|
||||
self.assertTrue(frappe.response.docinfo, msg=f"expected docinfo in reponse, found: {frappe.response.docinfo}")
|
||||
try:
|
||||
getdoc(doctype, docname)
|
||||
except Exception as e:
|
||||
self.fail(f"Failed to load {doctype}-{docname}: {e}")
|
||||
|
||||
self.assertTrue(frappe.response.docs, msg=f"expected document in reponse, found: {frappe.response.docs}")
|
||||
self.assertTrue(frappe.response.docinfo, msg=f"expected docinfo in reponse, found: {frappe.response.docinfo}")
|
||||
|
||||
def assertDocPrint(self, doctype, docname):
|
||||
doc = frappe.get_doc(doctype, docname)
|
||||
doc.set("__onload", frappe._dict())
|
||||
doc.run_method("onload")
|
||||
|
||||
messages_before = frappe.get_message_log()
|
||||
ret = get_html_and_style(doc=doc.as_json(), print_format="Standard", no_letterhead=1)
|
||||
messages_after = frappe.get_message_log()
|
||||
|
||||
if len(messages_after) > len(messages_before):
|
||||
new_messages = messages_after[len(messages_before):]
|
||||
self.fail("Print view showing error/warnings: \n"
|
||||
+ "\n".join(str(msg) for msg in new_messages))
|
||||
|
||||
# html should exist
|
||||
self.assertTrue(bool(ret["html"]))
|
||||
|
@ -1163,7 +1163,8 @@ Government,Regierung,
|
||||
Grand Total,Gesamtbetrag,
|
||||
Grant,Gewähren,
|
||||
Grant Application,Antrag bewilligen,
|
||||
Grant Leaves,Grant Blätter,
|
||||
Grant Commission,Provision gewähren,
|
||||
Grant Leaves,Meldungen gewähren,
|
||||
Grant information.,Gewähren Sie Informationen.,
|
||||
Grocery,Lebensmittelgeschäft,
|
||||
Gross Pay,Bruttolohn,
|
||||
@ -1332,6 +1333,7 @@ Invoices for Costumers.,Rechnungen für Kunden.,
|
||||
Inward supplies from ISD,Nachschub von ISD,
|
||||
Inward supplies liable to reverse charge (other than 1 & 2 above),Rückbelastungspflichtige Lieferungen (außer 1 & 2 oben),
|
||||
Is Active,Ist aktiv(iert),
|
||||
Is Debit Note,Ist Lastschrift,
|
||||
Is Default,Ist Standard,
|
||||
Is Existing Asset,Vermögenswert existiert bereits.,
|
||||
Is Frozen,Ist gesperrt,
|
||||
|
Can't render this file because it is too large.
|
@ -31,24 +31,6 @@
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<script>
|
||||
frappe.ready(() => {
|
||||
$('.product-filter-filter').on('keydown', frappe.utils.debounce((e) => {
|
||||
const $input = $(e.target);
|
||||
const keyword = ($input.val() || '').toLowerCase();
|
||||
const $filter_options = $input.next('.filter-options');
|
||||
|
||||
$filter_options.find('.custom-control').show();
|
||||
$filter_options.find('.custom-control').each((i, el) => {
|
||||
const $el = $(el);
|
||||
const value = $el.data('value').toLowerCase();
|
||||
if (!value.includes(keyword)) {
|
||||
$el.hide();
|
||||
}
|
||||
});
|
||||
}, 300));
|
||||
})
|
||||
</script>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user