mirror of
https://github.com/frappe/erpnext.git
synced 2026-06-08 15:42:52 +00:00
Merge branch 'version-13-hotfix' of https://github.com/frappe/erpnext into deferred_entry_freeze_v13
This commit is contained in:
6
.github/helper/documentation.py
vendored
6
.github/helper/documentation.py
vendored
@@ -34,9 +34,9 @@ if __name__ == "__main__":
|
|||||||
|
|
||||||
if response.ok:
|
if response.ok:
|
||||||
payload = response.json()
|
payload = response.json()
|
||||||
title = payload.get("title", "").lower().strip()
|
title = (payload.get("title") or "").lower().strip()
|
||||||
head_sha = payload.get("head", {}).get("sha")
|
head_sha = (payload.get("head") or {}).get("sha")
|
||||||
body = payload.get("body", "").lower()
|
body = (payload.get("body") or "").lower()
|
||||||
|
|
||||||
if (title.startswith("feat")
|
if (title.startswith("feat")
|
||||||
and head_sha
|
and head_sha
|
||||||
|
|||||||
@@ -79,7 +79,6 @@ frappe.ui.form.on('Chart of Accounts Importer', {
|
|||||||
$(frm.fields_dict['chart_tree'].wrapper).empty(); // empty wrapper on removing file
|
$(frm.fields_dict['chart_tree'].wrapper).empty(); // empty wrapper on removing file
|
||||||
} else {
|
} else {
|
||||||
generate_tree_preview(frm);
|
generate_tree_preview(frm);
|
||||||
validate_csv_data(frm);
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -104,23 +103,6 @@ frappe.ui.form.on('Chart of Accounts Importer', {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
var validate_csv_data = function(frm) {
|
|
||||||
frappe.call({
|
|
||||||
method: "erpnext.accounts.doctype.chart_of_accounts_importer.chart_of_accounts_importer.validate_accounts",
|
|
||||||
args: {file_name: frm.doc.import_file},
|
|
||||||
callback: function(r) {
|
|
||||||
if(r.message && r.message[0]===true) {
|
|
||||||
frm.page["show_import_button"] = true;
|
|
||||||
frm.page["total_accounts"] = r.message[1];
|
|
||||||
frm.trigger("refresh");
|
|
||||||
} else {
|
|
||||||
frm.page.set_indicator(__('Resolve error and upload again.'), 'orange');
|
|
||||||
frappe.throw(__(r.message));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
var create_import_button = function(frm) {
|
var create_import_button = function(frm) {
|
||||||
frm.page.set_primary_action(__("Import"), function () {
|
frm.page.set_primary_action(__("Import"), function () {
|
||||||
frappe.call({
|
frappe.call({
|
||||||
@@ -151,6 +133,7 @@ var create_reset_button = function(frm) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
var generate_tree_preview = function(frm) {
|
var generate_tree_preview = function(frm) {
|
||||||
|
if (frm.doc.import_file) {
|
||||||
let parent = __('All Accounts');
|
let parent = __('All Accounts');
|
||||||
$(frm.fields_dict['chart_tree'].wrapper).empty(); // empty wrapper to load new data
|
$(frm.fields_dict['chart_tree'].wrapper).empty(); // empty wrapper to load new data
|
||||||
|
|
||||||
@@ -170,4 +153,5 @@ var generate_tree_preview = function(frm) {
|
|||||||
parent = node.value;
|
parent = node.value;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -25,8 +25,16 @@ from erpnext.accounts.doctype.account.chart_of_accounts.chart_of_accounts import
|
|||||||
|
|
||||||
|
|
||||||
class ChartofAccountsImporter(Document):
|
class ChartofAccountsImporter(Document):
|
||||||
def validate(self):
|
pass
|
||||||
validate_accounts(self.import_file)
|
|
||||||
|
def validate_columns(data):
|
||||||
|
if not data:
|
||||||
|
frappe.throw(_('No data found. Seems like you uploaded a blank file'))
|
||||||
|
|
||||||
|
no_of_columns = max([len(d) for d in data])
|
||||||
|
|
||||||
|
if no_of_columns > 7:
|
||||||
|
frappe.throw(_('More columns found than expected. Please compare the uploaded file with standard template'))
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def validate_company(company):
|
def validate_company(company):
|
||||||
@@ -131,6 +139,8 @@ def get_coa(doctype, parent, is_root=False, file_name=None):
|
|||||||
else:
|
else:
|
||||||
data = generate_data_from_excel(file_doc, extension)
|
data = generate_data_from_excel(file_doc, extension)
|
||||||
|
|
||||||
|
validate_columns(data)
|
||||||
|
validate_accounts(data)
|
||||||
forest = build_forest(data)
|
forest = build_forest(data)
|
||||||
accounts = build_tree_from_json("", chart_data=forest) # returns alist of dict in a tree render-able form
|
accounts = build_tree_from_json("", chart_data=forest) # returns alist of dict in a tree render-able form
|
||||||
|
|
||||||
@@ -322,9 +332,6 @@ def validate_accounts(file_name):
|
|||||||
|
|
||||||
def validate_root(accounts):
|
def validate_root(accounts):
|
||||||
roots = [accounts[d] for d in accounts if not accounts[d].get('parent_account')]
|
roots = [accounts[d] for d in accounts if not accounts[d].get('parent_account')]
|
||||||
if len(roots) < 4:
|
|
||||||
frappe.throw(_("Number of root accounts cannot be less than 4"))
|
|
||||||
|
|
||||||
error_messages = []
|
error_messages = []
|
||||||
|
|
||||||
for account in roots:
|
for account in roots:
|
||||||
@@ -364,20 +371,12 @@ def get_mandatory_account_types():
|
|||||||
|
|
||||||
def validate_account_types(accounts):
|
def validate_account_types(accounts):
|
||||||
account_types_for_ledger = ["Cost of Goods Sold", "Depreciation", "Fixed Asset", "Payable", "Receivable", "Stock Adjustment"]
|
account_types_for_ledger = ["Cost of Goods Sold", "Depreciation", "Fixed Asset", "Payable", "Receivable", "Stock Adjustment"]
|
||||||
account_types = [accounts[d]["account_type"] for d in accounts if not accounts[d]['is_group'] == 1]
|
account_types = [accounts[d]["account_type"] for d in accounts if not cint(accounts[d]['is_group']) == 1]
|
||||||
|
|
||||||
missing = list(set(account_types_for_ledger) - set(account_types))
|
missing = list(set(account_types_for_ledger) - set(account_types))
|
||||||
if missing:
|
if missing:
|
||||||
frappe.throw(_("Please identify/create Account (Ledger) for type - {0}").format(' , '.join(missing)))
|
frappe.throw(_("Please identify/create Account (Ledger) for type - {0}").format(' , '.join(missing)))
|
||||||
|
|
||||||
account_types_for_group = ["Bank", "Cash", "Stock"]
|
|
||||||
# fix logic bug
|
|
||||||
account_groups = [accounts[d]["account_type"] for d in accounts if accounts[d]['is_group'] == 1]
|
|
||||||
|
|
||||||
missing = list(set(account_types_for_group) - set(account_groups))
|
|
||||||
if missing:
|
|
||||||
frappe.throw(_("Please identify/create Account (Group) for type - {0}").format(' , '.join(missing)))
|
|
||||||
|
|
||||||
def unset_existing_data(company):
|
def unset_existing_data(company):
|
||||||
linked = frappe.db.sql('''select fieldname from tabDocField
|
linked = frappe.db.sql('''select fieldname from tabDocField
|
||||||
where fieldtype="Link" and options="Account" and parent="Company"''', as_dict=True)
|
where fieldtype="Link" and options="Account" and parent="Company"''', as_dict=True)
|
||||||
|
|||||||
@@ -93,6 +93,7 @@
|
|||||||
"options": "Payment Term"
|
"options": "Payment Term"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"depends_on": "exchange_gain_loss",
|
||||||
"fieldname": "exchange_gain_loss",
|
"fieldname": "exchange_gain_loss",
|
||||||
"fieldtype": "Currency",
|
"fieldtype": "Currency",
|
||||||
"label": "Exchange Gain/Loss",
|
"label": "Exchange Gain/Loss",
|
||||||
@@ -103,7 +104,7 @@
|
|||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"istable": 1,
|
"istable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2021-04-21 13:30:11.605388",
|
"modified": "2021-09-26 17:06:55.597389",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Accounts",
|
"module": "Accounts",
|
||||||
"name": "Payment Entry Reference",
|
"name": "Payment Entry Reference",
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
"field_order": [
|
"field_order": [
|
||||||
"reference_type",
|
"reference_type",
|
||||||
"reference_name",
|
"reference_name",
|
||||||
|
"reference_row",
|
||||||
"column_break_3",
|
"column_break_3",
|
||||||
"invoice_type",
|
"invoice_type",
|
||||||
"invoice_number",
|
"invoice_number",
|
||||||
@@ -121,11 +122,17 @@
|
|||||||
"label": "Amount",
|
"label": "Amount",
|
||||||
"options": "Currency",
|
"options": "Currency",
|
||||||
"read_only": 1
|
"read_only": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "reference_row",
|
||||||
|
"fieldtype": "Data",
|
||||||
|
"hidden": 1,
|
||||||
|
"label": "Reference Row"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"istable": 1,
|
"istable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2021-08-30 10:58:42.665107",
|
"modified": "2021-09-20 17:23:09.455803",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Accounts",
|
"module": "Accounts",
|
||||||
"name": "Payment Reconciliation Allocation",
|
"name": "Payment Reconciliation Allocation",
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
frappe.ui.form.on('POS Invoice Merge Log', {
|
frappe.ui.form.on('POS Invoice Merge Log', {
|
||||||
setup: function(frm) {
|
setup: function(frm) {
|
||||||
frm.set_query("pos_invoice", "pos_invoices", doc => {
|
frm.set_query("pos_invoice", "pos_invoices", doc => {
|
||||||
return{
|
return {
|
||||||
filters: {
|
filters: {
|
||||||
'docstatus': 1,
|
'docstatus': 1,
|
||||||
'customer': doc.customer,
|
'customer': doc.customer,
|
||||||
@@ -12,5 +12,10 @@ frappe.ui.form.on('POS Invoice Merge Log', {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
merge_invoices_based_on: function(frm) {
|
||||||
|
frm.set_value('customer', '');
|
||||||
|
frm.set_value('customer_group', '');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -6,9 +6,11 @@
|
|||||||
"engine": "InnoDB",
|
"engine": "InnoDB",
|
||||||
"field_order": [
|
"field_order": [
|
||||||
"posting_date",
|
"posting_date",
|
||||||
"customer",
|
"merge_invoices_based_on",
|
||||||
"column_break_3",
|
"column_break_3",
|
||||||
"pos_closing_entry",
|
"pos_closing_entry",
|
||||||
|
"customer",
|
||||||
|
"customer_group",
|
||||||
"section_break_3",
|
"section_break_3",
|
||||||
"pos_invoices",
|
"pos_invoices",
|
||||||
"references_section",
|
"references_section",
|
||||||
@@ -88,12 +90,27 @@
|
|||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"label": "POS Closing Entry",
|
"label": "POS Closing Entry",
|
||||||
"options": "POS Closing Entry"
|
"options": "POS Closing Entry"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "merge_invoices_based_on",
|
||||||
|
"fieldtype": "Select",
|
||||||
|
"label": "Merge Invoices Based On",
|
||||||
|
"options": "Customer\nCustomer Group",
|
||||||
|
"reqd": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"depends_on": "eval:doc.merge_invoices_based_on == 'Customer Group'",
|
||||||
|
"fieldname": "customer_group",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"label": "Customer Group",
|
||||||
|
"mandatory_depends_on": "eval:doc.merge_invoices_based_on == 'Customer Group'",
|
||||||
|
"options": "Customer Group"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"is_submittable": 1,
|
"is_submittable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2020-12-01 11:53:57.267579",
|
"modified": "2021-09-14 11:17:19.001142",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Accounts",
|
"module": "Accounts",
|
||||||
"name": "POS Invoice Merge Log",
|
"name": "POS Invoice Merge Log",
|
||||||
|
|||||||
@@ -23,6 +23,9 @@ class POSInvoiceMergeLog(Document):
|
|||||||
self.validate_pos_invoice_status()
|
self.validate_pos_invoice_status()
|
||||||
|
|
||||||
def validate_customer(self):
|
def validate_customer(self):
|
||||||
|
if self.merge_invoices_based_on == 'Customer Group':
|
||||||
|
return
|
||||||
|
|
||||||
for d in self.pos_invoices:
|
for d in self.pos_invoices:
|
||||||
if d.customer != self.customer:
|
if d.customer != self.customer:
|
||||||
frappe.throw(_("Row #{}: POS Invoice {} is not against customer {}").format(d.idx, d.pos_invoice, self.customer))
|
frappe.throw(_("Row #{}: POS Invoice {} is not against customer {}").format(d.idx, d.pos_invoice, self.customer))
|
||||||
@@ -124,7 +127,7 @@ class POSInvoiceMergeLog(Document):
|
|||||||
found = False
|
found = False
|
||||||
for i in items:
|
for i in items:
|
||||||
if (i.item_code == item.item_code and not i.serial_no and not i.batch_no and
|
if (i.item_code == item.item_code and not i.serial_no and not i.batch_no and
|
||||||
i.uom == item.uom and i.net_rate == item.net_rate):
|
i.uom == item.uom and i.net_rate == item.net_rate and i.warehouse == item.warehouse):
|
||||||
found = True
|
found = True
|
||||||
i.qty = i.qty + item.qty
|
i.qty = i.qty + item.qty
|
||||||
|
|
||||||
@@ -172,6 +175,11 @@ class POSInvoiceMergeLog(Document):
|
|||||||
invoice.discount_amount = 0.0
|
invoice.discount_amount = 0.0
|
||||||
invoice.taxes_and_charges = None
|
invoice.taxes_and_charges = None
|
||||||
invoice.ignore_pricing_rule = 1
|
invoice.ignore_pricing_rule = 1
|
||||||
|
invoice.customer = self.customer
|
||||||
|
|
||||||
|
if self.merge_invoices_based_on == 'Customer Group':
|
||||||
|
invoice.flags.ignore_pos_profile = True
|
||||||
|
invoice.pos_profile = ''
|
||||||
|
|
||||||
return invoice
|
return invoice
|
||||||
|
|
||||||
@@ -228,7 +236,7 @@ def get_all_unconsolidated_invoices():
|
|||||||
return pos_invoices
|
return pos_invoices
|
||||||
|
|
||||||
def get_invoice_customer_map(pos_invoices):
|
def get_invoice_customer_map(pos_invoices):
|
||||||
# pos_invoice_customer_map = { 'Customer 1': [{}, {}, {}], 'Custoemr 2' : [{}] }
|
# pos_invoice_customer_map = { 'Customer 1': [{}, {}, {}], 'Customer 2' : [{}] }
|
||||||
pos_invoice_customer_map = {}
|
pos_invoice_customer_map = {}
|
||||||
for invoice in pos_invoices:
|
for invoice in pos_invoices:
|
||||||
customer = invoice.get('customer')
|
customer = invoice.get('customer')
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -15,6 +15,7 @@ from erpnext.accounts.deferred_revenue import validate_service_stop_date
|
|||||||
from erpnext.accounts.doctype.gl_entry.gl_entry import update_outstanding_amt
|
from erpnext.accounts.doctype.gl_entry.gl_entry import update_outstanding_amt
|
||||||
from erpnext.accounts.doctype.sales_invoice.sales_invoice import (
|
from erpnext.accounts.doctype.sales_invoice.sales_invoice import (
|
||||||
check_if_return_invoice_linked_with_payment_entry,
|
check_if_return_invoice_linked_with_payment_entry,
|
||||||
|
is_overdue,
|
||||||
unlink_inter_company_doc,
|
unlink_inter_company_doc,
|
||||||
update_linked_doc,
|
update_linked_doc,
|
||||||
validate_inter_company_party,
|
validate_inter_company_party,
|
||||||
@@ -1139,10 +1140,7 @@ class PurchaseInvoice(BuyingController):
|
|||||||
self.status = 'Draft'
|
self.status = 'Draft'
|
||||||
return
|
return
|
||||||
|
|
||||||
precision = self.precision("outstanding_amount")
|
outstanding_amount = flt(self.outstanding_amount, self.precision("outstanding_amount"))
|
||||||
outstanding_amount = flt(self.outstanding_amount, precision)
|
|
||||||
due_date = getdate(self.due_date)
|
|
||||||
nowdate = getdate()
|
|
||||||
|
|
||||||
if not status:
|
if not status:
|
||||||
if self.docstatus == 2:
|
if self.docstatus == 2:
|
||||||
@@ -1150,9 +1148,11 @@ class PurchaseInvoice(BuyingController):
|
|||||||
elif self.docstatus == 1:
|
elif self.docstatus == 1:
|
||||||
if self.is_internal_transfer():
|
if self.is_internal_transfer():
|
||||||
self.status = 'Internal Transfer'
|
self.status = 'Internal Transfer'
|
||||||
elif outstanding_amount > 0 and due_date < nowdate:
|
elif is_overdue(self):
|
||||||
self.status = "Overdue"
|
self.status = "Overdue"
|
||||||
elif outstanding_amount > 0 and due_date >= nowdate:
|
elif 0 < outstanding_amount < flt(self.grand_total, self.precision("grand_total")):
|
||||||
|
self.status = "Partly Paid"
|
||||||
|
elif outstanding_amount > 0 and getdate(self.due_date) >= getdate():
|
||||||
self.status = "Unpaid"
|
self.status = "Unpaid"
|
||||||
#Check if outstanding amount is 0 due to debit note issued against invoice
|
#Check if outstanding amount is 0 due to debit note issued against invoice
|
||||||
elif outstanding_amount <= 0 and self.is_return == 0 and frappe.db.get_value('Purchase Invoice', {'is_return': 1, 'return_against': self.name, 'docstatus': 1}):
|
elif outstanding_amount <= 0 and self.is_return == 0 and frappe.db.get_value('Purchase Invoice', {'is_return': 1, 'return_against': self.name, 'docstatus': 1}):
|
||||||
|
|||||||
@@ -2,28 +2,58 @@
|
|||||||
// License: GNU General Public License v3. See license.txt
|
// License: GNU General Public License v3. See license.txt
|
||||||
|
|
||||||
// render
|
// render
|
||||||
frappe.listview_settings['Purchase Invoice'] = {
|
frappe.listview_settings["Purchase Invoice"] = {
|
||||||
add_fields: ["supplier", "supplier_name", "base_grand_total", "outstanding_amount", "due_date", "company",
|
add_fields: [
|
||||||
"currency", "is_return", "release_date", "on_hold", "represents_company", "is_internal_supplier"],
|
"supplier",
|
||||||
get_indicator: function(doc) {
|
"supplier_name",
|
||||||
if ((flt(doc.outstanding_amount) <= 0) && doc.docstatus == 1 && doc.status == 'Debit Note Issued') {
|
"base_grand_total",
|
||||||
return [__("Debit Note Issued"), "darkgrey", "outstanding_amount,<=,0"];
|
"outstanding_amount",
|
||||||
} else if (flt(doc.outstanding_amount) > 0 && doc.docstatus==1) {
|
"due_date",
|
||||||
if(cint(doc.on_hold) && !doc.release_date) {
|
"company",
|
||||||
|
"currency",
|
||||||
|
"is_return",
|
||||||
|
"release_date",
|
||||||
|
"on_hold",
|
||||||
|
"represents_company",
|
||||||
|
"is_internal_supplier",
|
||||||
|
],
|
||||||
|
get_indicator(doc) {
|
||||||
|
if (doc.status == "Debit Note Issued") {
|
||||||
|
return [__(doc.status), "darkgrey", "status,=," + doc.status];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
flt(doc.outstanding_amount) > 0 &&
|
||||||
|
doc.docstatus == 1 &&
|
||||||
|
cint(doc.on_hold)
|
||||||
|
) {
|
||||||
|
if (!doc.release_date) {
|
||||||
return [__("On Hold"), "darkgrey"];
|
return [__("On Hold"), "darkgrey"];
|
||||||
} else if (cint(doc.on_hold) && doc.release_date && frappe.datetime.get_diff(doc.release_date, frappe.datetime.nowdate()) > 0) {
|
} else if (
|
||||||
|
frappe.datetime.get_diff(
|
||||||
|
doc.release_date,
|
||||||
|
frappe.datetime.nowdate()
|
||||||
|
) > 0
|
||||||
|
) {
|
||||||
return [__("Temporarily on Hold"), "darkgrey"];
|
return [__("Temporarily on Hold"), "darkgrey"];
|
||||||
} else if (frappe.datetime.get_diff(doc.due_date) < 0) {
|
|
||||||
return [__("Overdue"), "red", "outstanding_amount,>,0|due_date,<,Today"];
|
|
||||||
} else {
|
|
||||||
return [__("Unpaid"), "orange", "outstanding_amount,>,0|due_date,>=,Today"];
|
|
||||||
}
|
|
||||||
} else if (cint(doc.is_return)) {
|
|
||||||
return [__("Return"), "gray", "is_return,=,Yes"];
|
|
||||||
} else if (doc.company == doc.represents_company && doc.is_internal_supplier) {
|
|
||||||
return [__("Internal Transfer"), "darkgrey", "outstanding_amount,=,0"];
|
|
||||||
} else if (flt(doc.outstanding_amount)==0 && doc.docstatus==1) {
|
|
||||||
return [__("Paid"), "green", "outstanding_amount,=,0"];
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const status_colors = {
|
||||||
|
"Unpaid": "orange",
|
||||||
|
"Paid": "green",
|
||||||
|
"Return": "gray",
|
||||||
|
"Overdue": "red",
|
||||||
|
"Partly Paid": "yellow",
|
||||||
|
"Internal Transfer": "darkgrey",
|
||||||
|
};
|
||||||
|
|
||||||
|
if (status_colors[doc.status]) {
|
||||||
|
return [
|
||||||
|
__(doc.status),
|
||||||
|
status_colors[doc.status],
|
||||||
|
"status,=," + doc.status,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -97,6 +97,7 @@
|
|||||||
"width": "100px"
|
"width": "100px"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"depends_on": "exchange_gain_loss",
|
||||||
"fieldname": "exchange_gain_loss",
|
"fieldname": "exchange_gain_loss",
|
||||||
"fieldtype": "Currency",
|
"fieldtype": "Currency",
|
||||||
"label": "Exchange Gain/Loss",
|
"label": "Exchange Gain/Loss",
|
||||||
@@ -104,6 +105,7 @@
|
|||||||
"read_only": 1
|
"read_only": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"depends_on": "exchange_gain_loss",
|
||||||
"fieldname": "ref_exchange_rate",
|
"fieldname": "ref_exchange_rate",
|
||||||
"fieldtype": "Float",
|
"fieldtype": "Float",
|
||||||
"label": "Reference Exchange Rate",
|
"label": "Reference Exchange Rate",
|
||||||
@@ -115,7 +117,7 @@
|
|||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"istable": 1,
|
"istable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2021-04-20 16:26:53.820530",
|
"modified": "2021-09-26 15:47:28.167371",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Accounts",
|
"module": "Accounts",
|
||||||
"name": "Purchase Invoice Advance",
|
"name": "Purchase Invoice Advance",
|
||||||
|
|||||||
@@ -1652,7 +1652,7 @@
|
|||||||
"label": "Status",
|
"label": "Status",
|
||||||
"length": 30,
|
"length": 30,
|
||||||
"no_copy": 1,
|
"no_copy": 1,
|
||||||
"options": "\nDraft\nReturn\nCredit Note Issued\nSubmitted\nPaid\nUnpaid\nUnpaid and Discounted\nOverdue and Discounted\nOverdue\nCancelled\nInternal Transfer",
|
"options": "\nDraft\nReturn\nCredit Note Issued\nSubmitted\nPaid\nPartly Paid\nUnpaid\nUnpaid and Discounted\nPartly Paid and Discounted\nOverdue and Discounted\nOverdue\nCancelled\nInternal Transfer",
|
||||||
"print_hide": 1,
|
"print_hide": 1,
|
||||||
"read_only": 1
|
"read_only": 1
|
||||||
},
|
},
|
||||||
@@ -2032,11 +2032,12 @@
|
|||||||
"link_fieldname": "consolidated_invoice"
|
"link_fieldname": "consolidated_invoice"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"modified": "2021-09-08 15:24:25.486499",
|
"modified": "2021-09-21 09:27:50.191854",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Accounts",
|
"module": "Accounts",
|
||||||
"name": "Sales Invoice",
|
"name": "Sales Invoice",
|
||||||
"name_case": "Title Case",
|
"name_case": "Title Case",
|
||||||
|
"naming_rule": "By \"Naming Series\" field",
|
||||||
"owner": "Administrator",
|
"owner": "Administrator",
|
||||||
"permissions": [
|
"permissions": [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -501,7 +501,7 @@ class SalesInvoice(SellingController):
|
|||||||
self.account_for_change_amount = frappe.get_cached_value('Company', self.company, 'default_cash_account')
|
self.account_for_change_amount = frappe.get_cached_value('Company', self.company, 'default_cash_account')
|
||||||
|
|
||||||
from erpnext.stock.get_item_details import get_pos_profile, get_pos_profile_item_details
|
from erpnext.stock.get_item_details import get_pos_profile, get_pos_profile_item_details
|
||||||
if not self.pos_profile:
|
if not self.pos_profile and not self.flags.ignore_pos_profile:
|
||||||
pos_profile = get_pos_profile(self.company) or {}
|
pos_profile = get_pos_profile(self.company) or {}
|
||||||
if not pos_profile:
|
if not pos_profile:
|
||||||
return
|
return
|
||||||
@@ -1472,14 +1472,7 @@ class SalesInvoice(SellingController):
|
|||||||
self.status = 'Draft'
|
self.status = 'Draft'
|
||||||
return
|
return
|
||||||
|
|
||||||
precision = self.precision("outstanding_amount")
|
outstanding_amount = flt(self.outstanding_amount, self.precision("outstanding_amount"))
|
||||||
outstanding_amount = flt(self.outstanding_amount, precision)
|
|
||||||
due_date = getdate(self.due_date)
|
|
||||||
nowdate = getdate()
|
|
||||||
|
|
||||||
discounting_status = None
|
|
||||||
if self.is_discounted:
|
|
||||||
discounting_status = get_discounting_status(self.name)
|
|
||||||
|
|
||||||
if not status:
|
if not status:
|
||||||
if self.docstatus == 2:
|
if self.docstatus == 2:
|
||||||
@@ -1487,15 +1480,13 @@ class SalesInvoice(SellingController):
|
|||||||
elif self.docstatus == 1:
|
elif self.docstatus == 1:
|
||||||
if self.is_internal_transfer():
|
if self.is_internal_transfer():
|
||||||
self.status = 'Internal Transfer'
|
self.status = 'Internal Transfer'
|
||||||
elif outstanding_amount > 0 and due_date < nowdate and self.is_discounted and discounting_status=='Disbursed':
|
elif is_overdue(self):
|
||||||
self.status = "Overdue and Discounted"
|
|
||||||
elif outstanding_amount > 0 and due_date < nowdate:
|
|
||||||
self.status = "Overdue"
|
self.status = "Overdue"
|
||||||
elif outstanding_amount > 0 and due_date >= nowdate and self.is_discounted and discounting_status=='Disbursed':
|
elif 0 < outstanding_amount < flt(self.grand_total, self.precision("grand_total")):
|
||||||
self.status = "Unpaid and Discounted"
|
self.status = "Partly Paid"
|
||||||
elif outstanding_amount > 0 and due_date >= nowdate:
|
elif outstanding_amount > 0 and getdate(self.due_date) >= getdate():
|
||||||
self.status = "Unpaid"
|
self.status = "Unpaid"
|
||||||
#Check if outstanding amount is 0 due to credit note issued against invoice
|
# Check if outstanding amount is 0 due to credit note issued against invoice
|
||||||
elif outstanding_amount <= 0 and self.is_return == 0 and frappe.db.get_value('Sales Invoice', {'is_return': 1, 'return_against': self.name, 'docstatus': 1}):
|
elif outstanding_amount <= 0 and self.is_return == 0 and frappe.db.get_value('Sales Invoice', {'is_return': 1, 'return_against': self.name, 'docstatus': 1}):
|
||||||
self.status = "Credit Note Issued"
|
self.status = "Credit Note Issued"
|
||||||
elif self.is_return == 1:
|
elif self.is_return == 1:
|
||||||
@@ -1504,12 +1495,42 @@ class SalesInvoice(SellingController):
|
|||||||
self.status = "Paid"
|
self.status = "Paid"
|
||||||
else:
|
else:
|
||||||
self.status = "Submitted"
|
self.status = "Submitted"
|
||||||
|
|
||||||
|
if (
|
||||||
|
self.status in ("Unpaid", "Partly Paid", "Overdue")
|
||||||
|
and self.is_discounted
|
||||||
|
and get_discounting_status(self.name) == "Disbursed"
|
||||||
|
):
|
||||||
|
self.status += " and Discounted"
|
||||||
|
|
||||||
else:
|
else:
|
||||||
self.status = "Draft"
|
self.status = "Draft"
|
||||||
|
|
||||||
if update:
|
if update:
|
||||||
self.db_set('status', self.status, update_modified = update_modified)
|
self.db_set('status', self.status, update_modified = update_modified)
|
||||||
|
|
||||||
|
def is_overdue(doc):
|
||||||
|
outstanding_amount = flt(doc.outstanding_amount, doc.precision("outstanding_amount"))
|
||||||
|
|
||||||
|
if outstanding_amount <= 0:
|
||||||
|
return
|
||||||
|
|
||||||
|
grand_total = flt(doc.grand_total, doc.precision("grand_total"))
|
||||||
|
nowdate = getdate()
|
||||||
|
if doc.payment_schedule:
|
||||||
|
# calculate payable amount till date
|
||||||
|
payable_amount = sum(
|
||||||
|
payment.payment_amount
|
||||||
|
for payment in doc.payment_schedule
|
||||||
|
if getdate(payment.due_date) < nowdate
|
||||||
|
)
|
||||||
|
|
||||||
|
if (grand_total - outstanding_amount) < payable_amount:
|
||||||
|
return True
|
||||||
|
|
||||||
|
elif getdate(doc.due_date) < nowdate:
|
||||||
|
return True
|
||||||
|
|
||||||
def get_discounting_status(sales_invoice):
|
def get_discounting_status(sales_invoice):
|
||||||
status = None
|
status = None
|
||||||
|
|
||||||
|
|||||||
@@ -6,18 +6,20 @@ frappe.listview_settings['Sales Invoice'] = {
|
|||||||
add_fields: ["customer", "customer_name", "base_grand_total", "outstanding_amount", "due_date", "company",
|
add_fields: ["customer", "customer_name", "base_grand_total", "outstanding_amount", "due_date", "company",
|
||||||
"currency", "is_return"],
|
"currency", "is_return"],
|
||||||
get_indicator: function(doc) {
|
get_indicator: function(doc) {
|
||||||
var status_color = {
|
const status_colors = {
|
||||||
"Draft": "grey",
|
"Draft": "grey",
|
||||||
"Unpaid": "orange",
|
"Unpaid": "orange",
|
||||||
"Paid": "green",
|
"Paid": "green",
|
||||||
"Return": "gray",
|
"Return": "gray",
|
||||||
"Credit Note Issued": "gray",
|
"Credit Note Issued": "gray",
|
||||||
"Unpaid and Discounted": "orange",
|
"Unpaid and Discounted": "orange",
|
||||||
|
"Partly Paid and Discounted": "yellow",
|
||||||
"Overdue and Discounted": "red",
|
"Overdue and Discounted": "red",
|
||||||
"Overdue": "red",
|
"Overdue": "red",
|
||||||
|
"Partly Paid": "yellow",
|
||||||
"Internal Transfer": "darkgrey"
|
"Internal Transfer": "darkgrey"
|
||||||
};
|
};
|
||||||
return [__(doc.status), status_color[doc.status], "status,=,"+doc.status];
|
return [__(doc.status), status_colors[doc.status], "status,=,"+doc.status];
|
||||||
},
|
},
|
||||||
right_column: "grand_total"
|
right_column: "grand_total"
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -131,6 +131,7 @@ class TestSalesInvoice(unittest.TestCase):
|
|||||||
|
|
||||||
def test_payment_entry_unlink_against_invoice(self):
|
def test_payment_entry_unlink_against_invoice(self):
|
||||||
from erpnext.accounts.doctype.payment_entry.test_payment_entry import get_payment_entry
|
from erpnext.accounts.doctype.payment_entry.test_payment_entry import get_payment_entry
|
||||||
|
|
||||||
si = frappe.copy_doc(test_records[0])
|
si = frappe.copy_doc(test_records[0])
|
||||||
si.is_pos = 0
|
si.is_pos = 0
|
||||||
si.insert()
|
si.insert()
|
||||||
@@ -154,6 +155,7 @@ class TestSalesInvoice(unittest.TestCase):
|
|||||||
|
|
||||||
def test_payment_entry_unlink_against_standalone_credit_note(self):
|
def test_payment_entry_unlink_against_standalone_credit_note(self):
|
||||||
from erpnext.accounts.doctype.payment_entry.test_payment_entry import get_payment_entry
|
from erpnext.accounts.doctype.payment_entry.test_payment_entry import get_payment_entry
|
||||||
|
|
||||||
si1 = create_sales_invoice(rate=1000)
|
si1 = create_sales_invoice(rate=1000)
|
||||||
si2 = create_sales_invoice(rate=300)
|
si2 = create_sales_invoice(rate=300)
|
||||||
si3 = create_sales_invoice(qty=-1, rate=300, is_return=1)
|
si3 = create_sales_invoice(qty=-1, rate=300, is_return=1)
|
||||||
@@ -1646,6 +1648,7 @@ class TestSalesInvoice(unittest.TestCase):
|
|||||||
|
|
||||||
def test_credit_note(self):
|
def test_credit_note(self):
|
||||||
from erpnext.accounts.doctype.payment_entry.test_payment_entry import get_payment_entry
|
from erpnext.accounts.doctype.payment_entry.test_payment_entry import get_payment_entry
|
||||||
|
|
||||||
si = create_sales_invoice(item_code = "_Test Item", qty = (5 * -1), rate=500, is_return = 1)
|
si = create_sales_invoice(item_code = "_Test Item", qty = (5 * -1), rate=500, is_return = 1)
|
||||||
|
|
||||||
outstanding_amount = get_outstanding_amount(si.doctype,
|
outstanding_amount = get_outstanding_amount(si.doctype,
|
||||||
@@ -2275,6 +2278,54 @@ class TestSalesInvoice(unittest.TestCase):
|
|||||||
party_link.delete()
|
party_link.delete()
|
||||||
frappe.db.set_value('Accounts Settings', None, 'enable_common_party_accounting', 0)
|
frappe.db.set_value('Accounts Settings', None, 'enable_common_party_accounting', 0)
|
||||||
|
|
||||||
|
def test_payment_statuses(self):
|
||||||
|
from erpnext.accounts.doctype.payment_entry.test_payment_entry import get_payment_entry
|
||||||
|
|
||||||
|
today = nowdate()
|
||||||
|
|
||||||
|
# Test Overdue
|
||||||
|
si = create_sales_invoice(do_not_submit=True)
|
||||||
|
si.payment_schedule = []
|
||||||
|
si.append("payment_schedule", {
|
||||||
|
"due_date": add_days(today, -5),
|
||||||
|
"invoice_portion": 50,
|
||||||
|
"payment_amount": si.grand_total / 2
|
||||||
|
})
|
||||||
|
si.append("payment_schedule", {
|
||||||
|
"due_date": add_days(today, 5),
|
||||||
|
"invoice_portion": 50,
|
||||||
|
"payment_amount": si.grand_total / 2
|
||||||
|
})
|
||||||
|
si.submit()
|
||||||
|
self.assertEqual(si.status, "Overdue")
|
||||||
|
|
||||||
|
# Test payment less than due amount
|
||||||
|
pe = get_payment_entry("Sales Invoice", si.name, bank_account="_Test Bank - _TC")
|
||||||
|
pe.reference_no = "1"
|
||||||
|
pe.reference_date = nowdate()
|
||||||
|
pe.paid_amount = 1
|
||||||
|
pe.references[0].allocated_amount = pe.paid_amount
|
||||||
|
pe.submit()
|
||||||
|
si.reload()
|
||||||
|
self.assertEqual(si.status, "Overdue")
|
||||||
|
|
||||||
|
# Test Partly Paid
|
||||||
|
pe = frappe.copy_doc(pe)
|
||||||
|
pe.paid_amount = si.grand_total / 2
|
||||||
|
pe.references[0].allocated_amount = pe.paid_amount
|
||||||
|
pe.submit()
|
||||||
|
si.reload()
|
||||||
|
self.assertEqual(si.status, "Partly Paid")
|
||||||
|
|
||||||
|
# Test Paid
|
||||||
|
pe = get_payment_entry("Sales Invoice", si.name, bank_account="_Test Bank - _TC")
|
||||||
|
pe.reference_no = "1"
|
||||||
|
pe.reference_date = nowdate()
|
||||||
|
pe.paid_amount = si.outstanding_amount
|
||||||
|
pe.submit()
|
||||||
|
si.reload()
|
||||||
|
self.assertEqual(si.status, "Paid")
|
||||||
|
|
||||||
def get_sales_invoice_for_e_invoice():
|
def get_sales_invoice_for_e_invoice():
|
||||||
si = make_sales_invoice_for_ewaybill()
|
si = make_sales_invoice_for_ewaybill()
|
||||||
si.naming_series = 'INV-2020-.#####'
|
si.naming_series = 'INV-2020-.#####'
|
||||||
|
|||||||
@@ -98,6 +98,7 @@
|
|||||||
"width": "120px"
|
"width": "120px"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"depends_on": "exchange_gain_loss",
|
||||||
"fieldname": "exchange_gain_loss",
|
"fieldname": "exchange_gain_loss",
|
||||||
"fieldtype": "Currency",
|
"fieldtype": "Currency",
|
||||||
"label": "Exchange Gain/Loss",
|
"label": "Exchange Gain/Loss",
|
||||||
@@ -105,6 +106,7 @@
|
|||||||
"read_only": 1
|
"read_only": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"depends_on": "exchange_gain_loss",
|
||||||
"fieldname": "ref_exchange_rate",
|
"fieldname": "ref_exchange_rate",
|
||||||
"fieldtype": "Float",
|
"fieldtype": "Float",
|
||||||
"label": "Reference Exchange Rate",
|
"label": "Reference Exchange Rate",
|
||||||
@@ -116,7 +118,7 @@
|
|||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"istable": 1,
|
"istable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2021-06-04 20:25:49.832052",
|
"modified": "2021-09-26 15:47:46.911595",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Accounts",
|
"module": "Accounts",
|
||||||
"name": "Sales Invoice Advance",
|
"name": "Sales Invoice Advance",
|
||||||
|
|||||||
@@ -4,9 +4,10 @@
|
|||||||
frappe.query_reports["Unpaid Expense Claim"] = {
|
frappe.query_reports["Unpaid Expense Claim"] = {
|
||||||
"filters": [
|
"filters": [
|
||||||
{
|
{
|
||||||
"fieldname":"employee",
|
"fieldname": "employee",
|
||||||
"label": __("Employee"),
|
"label": __("Employee"),
|
||||||
"fieldtype": "Link"
|
"fieldtype": "Link",
|
||||||
|
"options": "Employee"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -394,10 +394,6 @@ class Asset(AccountsController):
|
|||||||
if cint(self.number_of_depreciations_booked) > cint(row.total_number_of_depreciations):
|
if cint(self.number_of_depreciations_booked) > cint(row.total_number_of_depreciations):
|
||||||
frappe.throw(_("Number of Depreciations Booked cannot be greater than Total Number of Depreciations"))
|
frappe.throw(_("Number of Depreciations Booked cannot be greater than Total Number of Depreciations"))
|
||||||
|
|
||||||
if row.depreciation_start_date and getdate(row.depreciation_start_date) < getdate(nowdate()):
|
|
||||||
frappe.msgprint(_("Depreciation Row {0}: Depreciation Start Date is entered as past date")
|
|
||||||
.format(row.idx), title=_('Warning'), indicator='red')
|
|
||||||
|
|
||||||
if row.depreciation_start_date and getdate(row.depreciation_start_date) < getdate(self.purchase_date):
|
if row.depreciation_start_date and getdate(row.depreciation_start_date) < getdate(self.purchase_date):
|
||||||
frappe.throw(_("Depreciation Row {0}: Next Depreciation Date cannot be before Purchase Date")
|
frappe.throw(_("Depreciation Row {0}: Next Depreciation Date cannot be before Purchase Date")
|
||||||
.format(row.idx))
|
.format(row.idx))
|
||||||
|
|||||||
@@ -645,12 +645,18 @@ class TestAsset(unittest.TestCase):
|
|||||||
pr = make_purchase_receipt(item_code="Macbook Pro",
|
pr = make_purchase_receipt(item_code="Macbook Pro",
|
||||||
qty=1, rate=8000.0, location="Test Location")
|
qty=1, rate=8000.0, location="Test Location")
|
||||||
|
|
||||||
|
finance_book = frappe.new_doc('Finance Book')
|
||||||
|
finance_book.finance_book_name = 'Income Tax'
|
||||||
|
finance_book.for_income_tax = 1
|
||||||
|
finance_book.insert(ignore_if_duplicate=1)
|
||||||
|
|
||||||
asset_name = frappe.db.get_value("Asset", {"purchase_receipt": pr.name}, 'name')
|
asset_name = frappe.db.get_value("Asset", {"purchase_receipt": pr.name}, 'name')
|
||||||
asset = frappe.get_doc('Asset', asset_name)
|
asset = frappe.get_doc('Asset', asset_name)
|
||||||
asset.calculate_depreciation = 1
|
asset.calculate_depreciation = 1
|
||||||
asset.available_for_use_date = '2030-07-12'
|
asset.available_for_use_date = '2030-07-12'
|
||||||
asset.purchase_date = '2030-01-01'
|
asset.purchase_date = '2030-01-01'
|
||||||
asset.append("finance_books", {
|
asset.append("finance_books", {
|
||||||
|
"finance_book": finance_book.name,
|
||||||
"expected_value_after_useful_life": 1000,
|
"expected_value_after_useful_life": 1000,
|
||||||
"depreciation_method": "Written Down Value",
|
"depreciation_method": "Written Down Value",
|
||||||
"total_number_of_depreciations": 3,
|
"total_number_of_depreciations": 3,
|
||||||
|
|||||||
@@ -433,12 +433,12 @@
|
|||||||
"image_field": "image",
|
"image_field": "image",
|
||||||
"links": [
|
"links": [
|
||||||
{
|
{
|
||||||
"group": "Item Group",
|
"group": "Allowed Items",
|
||||||
"link_doctype": "Supplier Item Group",
|
"link_doctype": "Party Specific Item",
|
||||||
"link_fieldname": "supplier"
|
"link_fieldname": "party"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"modified": "2021-08-27 18:02:44.314077",
|
"modified": "2021-09-06 17:37:56.522233",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Buying",
|
"module": "Buying",
|
||||||
"name": "Supplier",
|
"name": "Supplier",
|
||||||
|
|||||||
@@ -1,77 +0,0 @@
|
|||||||
{
|
|
||||||
"actions": [],
|
|
||||||
"creation": "2021-05-07 18:16:40.621421",
|
|
||||||
"doctype": "DocType",
|
|
||||||
"editable_grid": 1,
|
|
||||||
"engine": "InnoDB",
|
|
||||||
"field_order": [
|
|
||||||
"supplier",
|
|
||||||
"item_group"
|
|
||||||
],
|
|
||||||
"fields": [
|
|
||||||
{
|
|
||||||
"fieldname": "supplier",
|
|
||||||
"fieldtype": "Link",
|
|
||||||
"in_list_view": 1,
|
|
||||||
"label": "Supplier",
|
|
||||||
"options": "Supplier",
|
|
||||||
"reqd": 1
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldname": "item_group",
|
|
||||||
"fieldtype": "Link",
|
|
||||||
"in_list_view": 1,
|
|
||||||
"label": "Item Group",
|
|
||||||
"options": "Item Group",
|
|
||||||
"reqd": 1
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"index_web_pages_for_search": 1,
|
|
||||||
"links": [],
|
|
||||||
"modified": "2021-05-19 13:48:16.742303",
|
|
||||||
"modified_by": "Administrator",
|
|
||||||
"module": "Buying",
|
|
||||||
"name": "Supplier Item Group",
|
|
||||||
"owner": "Administrator",
|
|
||||||
"permissions": [
|
|
||||||
{
|
|
||||||
"create": 1,
|
|
||||||
"delete": 1,
|
|
||||||
"email": 1,
|
|
||||||
"export": 1,
|
|
||||||
"print": 1,
|
|
||||||
"read": 1,
|
|
||||||
"report": 1,
|
|
||||||
"role": "System Manager",
|
|
||||||
"share": 1,
|
|
||||||
"write": 1
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"create": 1,
|
|
||||||
"delete": 1,
|
|
||||||
"email": 1,
|
|
||||||
"export": 1,
|
|
||||||
"print": 1,
|
|
||||||
"read": 1,
|
|
||||||
"report": 1,
|
|
||||||
"role": "Purchase User",
|
|
||||||
"share": 1,
|
|
||||||
"write": 1
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"create": 1,
|
|
||||||
"delete": 1,
|
|
||||||
"email": 1,
|
|
||||||
"export": 1,
|
|
||||||
"print": 1,
|
|
||||||
"read": 1,
|
|
||||||
"report": 1,
|
|
||||||
"role": "Purchase Manager",
|
|
||||||
"share": 1,
|
|
||||||
"write": 1
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"sort_field": "modified",
|
|
||||||
"sort_order": "DESC",
|
|
||||||
"track_changes": 1
|
|
||||||
}
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
|
|
||||||
# For license information, please see license.txt
|
|
||||||
|
|
||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
import frappe
|
|
||||||
from frappe import _
|
|
||||||
from frappe.model.document import Document
|
|
||||||
|
|
||||||
|
|
||||||
class SupplierItemGroup(Document):
|
|
||||||
def validate(self):
|
|
||||||
exists = frappe.db.exists({
|
|
||||||
'doctype': 'Supplier Item Group',
|
|
||||||
'supplier': self.supplier,
|
|
||||||
'item_group': self.item_group
|
|
||||||
})
|
|
||||||
if exists:
|
|
||||||
frappe.throw(_("Item Group has already been linked to this supplier."))
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
|
|
||||||
# See license.txt
|
|
||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
# import frappe
|
|
||||||
import unittest
|
|
||||||
|
|
||||||
|
|
||||||
class TestSupplierItemGroup(unittest.TestCase):
|
|
||||||
pass
|
|
||||||
@@ -690,13 +690,17 @@ class AccountsController(TransactionBase):
|
|||||||
.format(d.reference_name, d.against_order))
|
.format(d.reference_name, d.against_order))
|
||||||
|
|
||||||
def set_advance_gain_or_loss(self):
|
def set_advance_gain_or_loss(self):
|
||||||
if not self.get("advances"):
|
if self.get('conversion_rate') == 1 or not self.get("advances"):
|
||||||
|
return
|
||||||
|
|
||||||
|
is_purchase_invoice = self.doctype == 'Purchase Invoice'
|
||||||
|
party_account = self.credit_to if is_purchase_invoice else self.debit_to
|
||||||
|
if get_account_currency(party_account) != self.currency:
|
||||||
return
|
return
|
||||||
|
|
||||||
for d in self.get("advances"):
|
for d in self.get("advances"):
|
||||||
advance_exchange_rate = d.ref_exchange_rate
|
advance_exchange_rate = d.ref_exchange_rate
|
||||||
if (d.allocated_amount and self.conversion_rate != 1
|
if (d.allocated_amount and self.conversion_rate != advance_exchange_rate):
|
||||||
and self.conversion_rate != advance_exchange_rate):
|
|
||||||
|
|
||||||
base_allocated_amount_in_ref_rate = advance_exchange_rate * d.allocated_amount
|
base_allocated_amount_in_ref_rate = advance_exchange_rate * d.allocated_amount
|
||||||
base_allocated_amount_in_inv_rate = self.conversion_rate * d.allocated_amount
|
base_allocated_amount_in_inv_rate = self.conversion_rate * d.allocated_amount
|
||||||
@@ -715,7 +719,7 @@ class AccountsController(TransactionBase):
|
|||||||
|
|
||||||
gain_loss_account = frappe.db.get_value('Company', self.company, 'exchange_gain_loss_account')
|
gain_loss_account = frappe.db.get_value('Company', self.company, 'exchange_gain_loss_account')
|
||||||
if not gain_loss_account:
|
if not gain_loss_account:
|
||||||
frappe.throw(_("Please set Default Exchange Gain/Loss Account in Company {}")
|
frappe.throw(_("Please set default Exchange Gain/Loss Account in Company {}")
|
||||||
.format(self.get('company')))
|
.format(self.get('company')))
|
||||||
account_currency = get_account_currency(gain_loss_account)
|
account_currency = get_account_currency(gain_loss_account)
|
||||||
if account_currency != self.company_currency:
|
if account_currency != self.company_currency:
|
||||||
@@ -734,7 +738,7 @@ class AccountsController(TransactionBase):
|
|||||||
"against": party,
|
"against": party,
|
||||||
dr_or_cr + "_in_account_currency": abs(d.exchange_gain_loss),
|
dr_or_cr + "_in_account_currency": abs(d.exchange_gain_loss),
|
||||||
dr_or_cr: abs(d.exchange_gain_loss),
|
dr_or_cr: abs(d.exchange_gain_loss),
|
||||||
"cost_center": self.cost_center,
|
"cost_center": self.cost_center or erpnext.get_default_cost_center(self.company),
|
||||||
"project": self.project
|
"project": self.project
|
||||||
}, item=d)
|
}, item=d)
|
||||||
)
|
)
|
||||||
@@ -985,15 +989,23 @@ class AccountsController(TransactionBase):
|
|||||||
item_allowance = {}
|
item_allowance = {}
|
||||||
global_qty_allowance, global_amount_allowance = None, None
|
global_qty_allowance, global_amount_allowance = None, None
|
||||||
|
|
||||||
|
role_allowed_to_over_bill = frappe.db.get_single_value('Accounts Settings', 'role_allowed_to_over_bill')
|
||||||
|
user_roles = frappe.get_roles()
|
||||||
|
|
||||||
|
total_overbilled_amt = 0.0
|
||||||
|
|
||||||
for item in self.get("items"):
|
for item in self.get("items"):
|
||||||
if item.get(item_ref_dn):
|
if not item.get(item_ref_dn):
|
||||||
|
continue
|
||||||
|
|
||||||
ref_amt = flt(frappe.db.get_value(ref_dt + " Item",
|
ref_amt = flt(frappe.db.get_value(ref_dt + " Item",
|
||||||
item.get(item_ref_dn), based_on), self.precision(based_on, item))
|
item.get(item_ref_dn), based_on), self.precision(based_on, item))
|
||||||
if not ref_amt:
|
if not ref_amt:
|
||||||
frappe.msgprint(
|
frappe.msgprint(
|
||||||
_("Warning: System will not check overbilling since amount for Item {0} in {1} is zero")
|
_("System will not check overbilling since amount for Item {0} in {1} is zero")
|
||||||
.format(item.item_code, ref_dt))
|
.format(item.item_code, ref_dt), title=_("Warning"), indicator="orange")
|
||||||
else:
|
continue
|
||||||
|
|
||||||
already_billed = frappe.db.sql("""
|
already_billed = frappe.db.sql("""
|
||||||
select sum(%s)
|
select sum(%s)
|
||||||
from `tab%s`
|
from `tab%s`
|
||||||
@@ -1014,14 +1026,19 @@ class AccountsController(TransactionBase):
|
|||||||
total_billed_amt = abs(total_billed_amt)
|
total_billed_amt = abs(total_billed_amt)
|
||||||
max_allowed_amt = abs(max_allowed_amt)
|
max_allowed_amt = abs(max_allowed_amt)
|
||||||
|
|
||||||
role_allowed_to_over_bill = frappe.db.get_single_value('Accounts Settings', 'role_allowed_to_over_bill')
|
overbill_amt = total_billed_amt - max_allowed_amt
|
||||||
|
total_overbilled_amt += overbill_amt
|
||||||
|
|
||||||
if total_billed_amt - max_allowed_amt > 0.01 and role_allowed_to_over_bill not in frappe.get_roles():
|
if overbill_amt > 0.01 and role_allowed_to_over_bill not in user_roles:
|
||||||
if self.doctype != "Purchase Invoice":
|
if self.doctype != "Purchase Invoice":
|
||||||
self.throw_overbill_exception(item, max_allowed_amt)
|
self.throw_overbill_exception(item, max_allowed_amt)
|
||||||
elif not cint(frappe.db.get_single_value("Buying Settings", "bill_for_rejected_quantity_in_purchase_invoice")):
|
elif not cint(frappe.db.get_single_value("Buying Settings", "bill_for_rejected_quantity_in_purchase_invoice")):
|
||||||
self.throw_overbill_exception(item, max_allowed_amt)
|
self.throw_overbill_exception(item, max_allowed_amt)
|
||||||
|
|
||||||
|
if role_allowed_to_over_bill in user_roles and total_overbilled_amt > 0.1:
|
||||||
|
frappe.msgprint(_("Overbilling of {} ignored because you have {} role.")
|
||||||
|
.format(total_overbilled_amt, role_allowed_to_over_bill), title=_("Warning"), indicator="orange")
|
||||||
|
|
||||||
def throw_overbill_exception(self, item, max_allowed_amt):
|
def throw_overbill_exception(self, item, max_allowed_amt):
|
||||||
frappe.throw(_("Cannot overbill for Item {0} in row {1} more than {2}. To allow over-billing, please set allowance in Accounts Settings")
|
frappe.throw(_("Cannot overbill for Item {0} in row {1} more than {2}. To allow over-billing, please set allowance in Accounts Settings")
|
||||||
.format(item.item_code, item.idx, max_allowed_amt))
|
.format(item.item_code, item.idx, max_allowed_amt))
|
||||||
@@ -1673,14 +1690,18 @@ def get_advance_payment_entries(party_type, party, party_account, order_doctype,
|
|||||||
return list(payment_entries_against_order) + list(unallocated_payment_entries)
|
return list(payment_entries_against_order) + list(unallocated_payment_entries)
|
||||||
|
|
||||||
def update_invoice_status():
|
def update_invoice_status():
|
||||||
# Daily update the status of the invoices
|
"""Updates status as Overdue for applicable invoices. Runs daily."""
|
||||||
|
|
||||||
frappe.db.sql(""" update `tabSales Invoice` set status = 'Overdue'
|
|
||||||
where due_date < CURDATE() and docstatus = 1 and outstanding_amount > 0""")
|
|
||||||
|
|
||||||
frappe.db.sql(""" update `tabPurchase Invoice` set status = 'Overdue'
|
|
||||||
where due_date < CURDATE() and docstatus = 1 and outstanding_amount > 0""")
|
|
||||||
|
|
||||||
|
for doctype in ("Sales Invoice", "Purchase Invoice"):
|
||||||
|
frappe.db.sql("""
|
||||||
|
update `tab{}` as dt set dt.status = 'Overdue'
|
||||||
|
where dt.docstatus = 1
|
||||||
|
and dt.status != 'Overdue'
|
||||||
|
and dt.outstanding_amount > 0
|
||||||
|
and (dt.grand_total - dt.outstanding_amount) <
|
||||||
|
(select sum(payment_amount) from `tabPayment Schedule` as ps
|
||||||
|
where ps.parent = dt.name and ps.due_date < %s)
|
||||||
|
""".format(doctype), getdate())
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def get_payment_terms(terms_template, posting_date=None, grand_total=None, base_grand_total=None, bill_date=None):
|
def get_payment_terms(terms_template, posting_date=None, grand_total=None, base_grand_total=None, bill_date=None):
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import json
|
|||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
|
from frappe import scrub
|
||||||
from frappe.desk.reportview import get_filters_cond, get_match_cond
|
from frappe.desk.reportview import get_filters_cond, get_match_cond
|
||||||
from frappe.utils import nowdate, unique
|
from frappe.utils import nowdate, unique
|
||||||
|
|
||||||
@@ -223,18 +224,29 @@ def item_query(doctype, txt, searchfield, start, page_len, filters, as_dict=Fals
|
|||||||
if not field in searchfields]
|
if not field in searchfields]
|
||||||
searchfields = " or ".join([field + " like %(txt)s" for field in searchfields])
|
searchfields = " or ".join([field + " like %(txt)s" for field in searchfields])
|
||||||
|
|
||||||
if filters and isinstance(filters, dict) and filters.get('supplier'):
|
if filters and isinstance(filters, dict):
|
||||||
item_group_list = frappe.get_all('Supplier Item Group',
|
if filters.get('customer') or filters.get('supplier'):
|
||||||
filters = {'supplier': filters.get('supplier')}, fields = ['item_group'])
|
party = filters.get('customer') or filters.get('supplier')
|
||||||
|
item_rules_list = frappe.get_all('Party Specific Item',
|
||||||
|
filters = {'party': party}, fields = ['restrict_based_on', 'based_on_value'])
|
||||||
|
|
||||||
item_groups = []
|
filters_dict = {}
|
||||||
for i in item_group_list:
|
for rule in item_rules_list:
|
||||||
item_groups.append(i.item_group)
|
if rule['restrict_based_on'] == 'Item':
|
||||||
|
rule['restrict_based_on'] = 'name'
|
||||||
|
filters_dict[rule.restrict_based_on] = []
|
||||||
|
|
||||||
|
for rule in item_rules_list:
|
||||||
|
filters_dict[rule.restrict_based_on].append(rule.based_on_value)
|
||||||
|
|
||||||
|
for filter in filters_dict:
|
||||||
|
filters[scrub(filter)] = ['in', filters_dict[filter]]
|
||||||
|
|
||||||
|
if filters.get('customer'):
|
||||||
|
del filters['customer']
|
||||||
|
else:
|
||||||
del filters['supplier']
|
del filters['supplier']
|
||||||
|
|
||||||
if item_groups:
|
|
||||||
filters['item_group'] = ['in', item_groups]
|
|
||||||
|
|
||||||
description_cond = ''
|
description_cond = ''
|
||||||
if frappe.db.count('Item', cache=True) < 50000:
|
if frappe.db.count('Item', cache=True) < 50000:
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import json
|
|||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
from frappe import _
|
from frappe import _
|
||||||
|
from frappe.modules.utils import get_module_app
|
||||||
from frappe.utils import flt, has_common
|
from frappe.utils import flt, has_common
|
||||||
from frappe.utils.user import is_website_user
|
from frappe.utils.user import is_website_user
|
||||||
|
|
||||||
@@ -21,8 +22,32 @@ def get_list_context(context=None):
|
|||||||
"get_list": get_transaction_list
|
"get_list": get_transaction_list
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def get_webform_list_context(module):
|
||||||
|
if get_module_app(module) != 'erpnext':
|
||||||
|
return
|
||||||
|
return {
|
||||||
|
"get_list": get_webform_transaction_list
|
||||||
|
}
|
||||||
|
|
||||||
def get_transaction_list(doctype, txt=None, filters=None, limit_start=0, limit_page_length=20, order_by="modified"):
|
def get_webform_transaction_list(doctype, txt=None, filters=None, limit_start=0, limit_page_length=20, order_by="modified"):
|
||||||
|
""" Get List of transactions for custom doctypes """
|
||||||
|
from frappe.www.list import get_list
|
||||||
|
|
||||||
|
if not filters:
|
||||||
|
filters = []
|
||||||
|
|
||||||
|
meta = frappe.get_meta(doctype)
|
||||||
|
|
||||||
|
for d in meta.fields:
|
||||||
|
if d.fieldtype == 'Link' and d.fieldname != 'amended_from':
|
||||||
|
allowed_docs = [d.name for d in get_transaction_list(doctype=d.options, custom=True)]
|
||||||
|
allowed_docs.append('')
|
||||||
|
filters.append((d.fieldname, 'in', allowed_docs))
|
||||||
|
|
||||||
|
return get_list(doctype, txt, filters, limit_start, limit_page_length, ignore_permissions=False,
|
||||||
|
fields=None, order_by="modified")
|
||||||
|
|
||||||
|
def get_transaction_list(doctype, txt=None, filters=None, limit_start=0, limit_page_length=20, order_by="modified", custom=False):
|
||||||
user = frappe.session.user
|
user = frappe.session.user
|
||||||
ignore_permissions = False
|
ignore_permissions = False
|
||||||
|
|
||||||
@@ -46,7 +71,7 @@ def get_transaction_list(doctype, txt=None, filters=None, limit_start=0, limit_p
|
|||||||
filters.append(('customer', 'in', customers))
|
filters.append(('customer', 'in', customers))
|
||||||
elif suppliers:
|
elif suppliers:
|
||||||
filters.append(('supplier', 'in', suppliers))
|
filters.append(('supplier', 'in', suppliers))
|
||||||
else:
|
elif not custom:
|
||||||
return []
|
return []
|
||||||
|
|
||||||
if doctype == 'Request for Quotation':
|
if doctype == 'Request for Quotation':
|
||||||
@@ -56,9 +81,16 @@ def get_transaction_list(doctype, txt=None, filters=None, limit_start=0, limit_p
|
|||||||
# Since customers and supplier do not have direct access to internal doctypes
|
# Since customers and supplier do not have direct access to internal doctypes
|
||||||
ignore_permissions = True
|
ignore_permissions = True
|
||||||
|
|
||||||
|
if not customers and not suppliers and custom:
|
||||||
|
ignore_permissions = False
|
||||||
|
filters = []
|
||||||
|
|
||||||
transactions = get_list_for_transactions(doctype, txt, filters, limit_start, limit_page_length,
|
transactions = get_list_for_transactions(doctype, txt, filters, limit_start, limit_page_length,
|
||||||
fields='name', ignore_permissions=ignore_permissions, order_by='modified desc')
|
fields='name', ignore_permissions=ignore_permissions, order_by='modified desc')
|
||||||
|
|
||||||
|
if custom:
|
||||||
|
return transactions
|
||||||
|
|
||||||
return post_process(doctype, transactions)
|
return post_process(doctype, transactions)
|
||||||
|
|
||||||
def get_list_for_transactions(doctype, txt, filters, limit_start, limit_page_length=20,
|
def get_list_for_transactions(doctype, txt, filters, limit_start, limit_page_length=20,
|
||||||
|
|||||||
@@ -12,8 +12,6 @@ from frappe.website.doctype.website_slideshow.website_slideshow import get_slide
|
|||||||
from frappe.website.website_generator import WebsiteGenerator
|
from frappe.website.website_generator import WebsiteGenerator
|
||||||
|
|
||||||
from erpnext.e_commerce.doctype.item_review.item_review import get_item_reviews
|
from erpnext.e_commerce.doctype.item_review.item_review import get_item_reviews
|
||||||
|
|
||||||
# SEARCH
|
|
||||||
from erpnext.e_commerce.redisearch import (
|
from erpnext.e_commerce.redisearch import (
|
||||||
delete_item_from_index,
|
delete_item_from_index,
|
||||||
insert_item_to_index,
|
insert_item_to_index,
|
||||||
@@ -138,10 +136,10 @@ class WebsiteItem(WebsiteGenerator):
|
|||||||
self.website_image = None
|
self.website_image = None
|
||||||
|
|
||||||
def make_thumbnail(self):
|
def make_thumbnail(self):
|
||||||
|
"""Make a thumbnail of `website_image`"""
|
||||||
if frappe.flags.in_import or frappe.flags.in_migrate:
|
if frappe.flags.in_import or frappe.flags.in_migrate:
|
||||||
return
|
return
|
||||||
|
|
||||||
"""Make a thumbnail of `website_image`"""
|
|
||||||
import requests.exceptions
|
import requests.exceptions
|
||||||
|
|
||||||
if not self.is_new() and self.website_image != frappe.db.get_value(self.doctype, self.name, "website_image"):
|
if not self.is_new() and self.website_image != frappe.db.get_value(self.doctype, self.name, "website_image"):
|
||||||
|
|||||||
@@ -20,14 +20,16 @@ def get_indexable_web_fields():
|
|||||||
return [df.fieldname for df in valid_fields]
|
return [df.fieldname for df in valid_fields]
|
||||||
|
|
||||||
def is_search_module_loaded():
|
def is_search_module_loaded():
|
||||||
|
try:
|
||||||
cache = frappe.cache()
|
cache = frappe.cache()
|
||||||
out = cache.execute_command('MODULE LIST')
|
out = cache.execute_command('MODULE LIST')
|
||||||
|
|
||||||
parsed_output = " ".join(
|
parsed_output = " ".join(
|
||||||
(" ".join([s.decode() for s in o if not isinstance(s, int)]) for o in out)
|
(" ".join([s.decode() for s in o if not isinstance(s, int)]) for o in out)
|
||||||
)
|
)
|
||||||
|
|
||||||
return "search" in parsed_output
|
return "search" in parsed_output
|
||||||
|
except Exception:
|
||||||
|
return False
|
||||||
|
|
||||||
def if_redisearch_loaded(function):
|
def if_redisearch_loaded(function):
|
||||||
"Decorator to check if Redisearch is loaded."
|
"Decorator to check if Redisearch is loaded."
|
||||||
|
|||||||
@@ -105,7 +105,7 @@ def place_order():
|
|||||||
if is_stock_item:
|
if is_stock_item:
|
||||||
item_stock = get_web_item_qty_in_stock(item.item_code, "website_warehouse")
|
item_stock = get_web_item_qty_in_stock(item.item_code, "website_warehouse")
|
||||||
if not cint(item_stock.in_stock):
|
if not cint(item_stock.in_stock):
|
||||||
throw(_("{1} Not in Stock").format(item.item_code))
|
throw(_("{0} Not in Stock").format(item.item_code))
|
||||||
if item.qty > item_stock.stock_qty[0][0]:
|
if item.qty > item_stock.stock_qty[0][0]:
|
||||||
throw(_("Only {0} in Stock for item {1}").format(item_stock.stock_qty[0][0], item.item_code))
|
throw(_("Only {0} in Stock for item {1}").format(item_stock.stock_qty[0][0], item.item_code))
|
||||||
|
|
||||||
@@ -168,8 +168,10 @@ def update_cart(item_code, qty, additional_notes=None, with_items=False):
|
|||||||
return {
|
return {
|
||||||
"items": frappe.render_template("templates/includes/cart/cart_items.html",
|
"items": frappe.render_template("templates/includes/cart/cart_items.html",
|
||||||
context),
|
context),
|
||||||
"taxes": frappe.render_template("templates/includes/order/order_taxes.html",
|
"total": frappe.render_template("templates/includes/cart/cart_items_total.html",
|
||||||
context),
|
context),
|
||||||
|
"taxes_and_totals": frappe.render_template("templates/includes/cart/cart_payment_summary.html",
|
||||||
|
context)
|
||||||
}
|
}
|
||||||
else:
|
else:
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -67,12 +67,16 @@ class ItemVariantsCacheManager:
|
|||||||
as_list=1
|
as_list=1
|
||||||
)
|
)
|
||||||
|
|
||||||
disabled_items = set([i.name for i in frappe.db.get_all('Item', {'disabled': 1})])
|
unpublished_items = set([i.item_code for i in frappe.db.get_all('Website Item', filters={'published': 0}, fields=["item_code"])])
|
||||||
|
|
||||||
attribute_value_item_map = frappe._dict({})
|
attribute_value_item_map = frappe._dict({})
|
||||||
item_attribute_value_map = frappe._dict({})
|
item_attribute_value_map = frappe._dict({})
|
||||||
|
|
||||||
item_variants_data = [r for r in item_variants_data if r[0] not in disabled_items]
|
# dont consider variants that are unpublished
|
||||||
|
# (either have no Website Item or are unpublished in Website Item)
|
||||||
|
item_variants_data = [r for r in item_variants_data if r[0] not in unpublished_items]
|
||||||
|
item_variants_data = [r for r in item_variants_data if frappe.db.exists("Website Item", {"item_code": r[0]})]
|
||||||
|
|
||||||
for row in item_variants_data:
|
for row in item_variants_data:
|
||||||
item_code, attribute, attribute_value = row
|
item_code, attribute, attribute_value = row
|
||||||
# (attr, value) => [item1, item2]
|
# (attr, value) => [item1, item2]
|
||||||
|
|||||||
@@ -85,10 +85,8 @@ def add_bank_accounts(response, bank, company):
|
|||||||
if not acc_subtype:
|
if not acc_subtype:
|
||||||
add_account_subtype(account["subtype"])
|
add_account_subtype(account["subtype"])
|
||||||
|
|
||||||
existing_bank_account = frappe.db.exists("Bank Account", {
|
bank_account_name = "{} - {}".format(account["name"], bank["bank_name"])
|
||||||
'account_name': account["name"],
|
existing_bank_account = frappe.db.exists("Bank Account", bank_account_name)
|
||||||
'bank': bank["bank_name"]
|
|
||||||
})
|
|
||||||
|
|
||||||
if not existing_bank_account:
|
if not existing_bank_account:
|
||||||
try:
|
try:
|
||||||
@@ -197,6 +195,7 @@ def get_transactions(bank, bank_account=None, start_date=None, end_date=None):
|
|||||||
|
|
||||||
plaid = PlaidConnector(access_token)
|
plaid = PlaidConnector(access_token)
|
||||||
|
|
||||||
|
transactions = []
|
||||||
try:
|
try:
|
||||||
transactions = plaid.get_transactions(start_date=start_date, end_date=end_date, account_id=account_id)
|
transactions = plaid.get_transactions(start_date=start_date, end_date=end_date, account_id=account_id)
|
||||||
except ItemError as e:
|
except ItemError as e:
|
||||||
@@ -205,7 +204,7 @@ def get_transactions(bank, bank_account=None, start_date=None, end_date=None):
|
|||||||
msg += _("Please refresh or reset the Plaid linking of the Bank {}.").format(bank) + " "
|
msg += _("Please refresh or reset the Plaid linking of the Bank {}.").format(bank) + " "
|
||||||
frappe.log_error(msg, title=_("Plaid Link Refresh Required"))
|
frappe.log_error(msg, title=_("Plaid Link Refresh Required"))
|
||||||
|
|
||||||
return transactions or []
|
return transactions
|
||||||
|
|
||||||
|
|
||||||
def new_bank_transaction(transaction):
|
def new_bank_transaction(transaction):
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ class Patient(Document):
|
|||||||
frappe.db.set_value('Patient', self.name, 'status', 'Disabled')
|
frappe.db.set_value('Patient', self.name, 'status', 'Disabled')
|
||||||
else:
|
else:
|
||||||
send_registration_sms(self)
|
send_registration_sms(self)
|
||||||
self.reload() # self.notify_update()
|
self.reload()
|
||||||
|
|
||||||
def on_update(self):
|
def on_update(self):
|
||||||
if frappe.db.get_single_value('Healthcare Settings', 'link_customer_to_patient'):
|
if frappe.db.get_single_value('Healthcare Settings', 'link_customer_to_patient'):
|
||||||
@@ -93,7 +93,11 @@ class Patient(Document):
|
|||||||
self.language = frappe.db.get_single_value('System Settings', 'language')
|
self.language = frappe.db.get_single_value('System Settings', 'language')
|
||||||
|
|
||||||
def create_website_user(self):
|
def create_website_user(self):
|
||||||
if self.email and not frappe.db.exists('User', self.email):
|
users = frappe.db.get_all('User', fields=['email', 'mobile_no'], or_filters={'email': self.email, 'mobile_no': self.mobile})
|
||||||
|
if users and users[0]:
|
||||||
|
frappe.throw(_("User exists with Email {}, Mobile {}<br>Please check email / mobile or disable 'Invite as User' to skip creating User")
|
||||||
|
.format(frappe.bold(users[0].email), frappe.bold(users[0].mobile_no)), frappe.DuplicateEntryError)
|
||||||
|
|
||||||
user = frappe.get_doc({
|
user = frappe.get_doc({
|
||||||
'doctype': 'User',
|
'doctype': 'User',
|
||||||
'first_name': self.first_name,
|
'first_name': self.first_name,
|
||||||
@@ -109,7 +113,7 @@ class Patient(Document):
|
|||||||
user.enabled = True
|
user.enabled = True
|
||||||
user.send_welcome_email = True
|
user.send_welcome_email = True
|
||||||
user.add_roles('Patient')
|
user.add_roles('Patient')
|
||||||
frappe.db.set_value(self.doctype, self.name, 'user_id', user.name)
|
self.db_set('user_id', user.name)
|
||||||
|
|
||||||
def autoname(self):
|
def autoname(self):
|
||||||
patient_name_by = frappe.db.get_single_value('Healthcare Settings', 'patient_name_by')
|
patient_name_by = frappe.db.get_single_value('Healthcare Settings', 'patient_name_by')
|
||||||
@@ -159,10 +163,22 @@ class Patient(Document):
|
|||||||
return {'invoice': sales_invoice.name}
|
return {'invoice': sales_invoice.name}
|
||||||
|
|
||||||
def set_contact(self):
|
def set_contact(self):
|
||||||
if frappe.db.exists('Dynamic Link', {'parenttype':'Contact', 'link_doctype':'Patient', 'link_name':self.name}):
|
contact = get_default_contact(self.doctype, self.name)
|
||||||
|
|
||||||
|
if contact:
|
||||||
old_doc = self.get_doc_before_save()
|
old_doc = self.get_doc_before_save()
|
||||||
|
if not old_doc:
|
||||||
|
return
|
||||||
|
|
||||||
if old_doc.email != self.email or old_doc.mobile != self.mobile or old_doc.phone != self.phone:
|
if old_doc.email != self.email or old_doc.mobile != self.mobile or old_doc.phone != self.phone:
|
||||||
self.update_contact()
|
self.update_contact(contact)
|
||||||
|
else:
|
||||||
|
if self.customer:
|
||||||
|
# customer contact exists, link patient
|
||||||
|
contact = get_default_contact('Customer', self.customer)
|
||||||
|
|
||||||
|
if contact:
|
||||||
|
self.update_contact(contact)
|
||||||
else:
|
else:
|
||||||
self.reload()
|
self.reload()
|
||||||
if self.email or self.mobile or self.phone:
|
if self.email or self.mobile or self.phone:
|
||||||
@@ -179,15 +195,14 @@ class Patient(Document):
|
|||||||
contact.append('links', dict(link_doctype='Customer', link_name=self.customer))
|
contact.append('links', dict(link_doctype='Customer', link_name=self.customer))
|
||||||
|
|
||||||
contact.insert(ignore_permissions=True)
|
contact.insert(ignore_permissions=True)
|
||||||
self.update_contact(contact) # update email, mobile and phone
|
self.update_contact(contact.name)
|
||||||
|
|
||||||
def update_contact(self, contact=None):
|
def update_contact(self, contact):
|
||||||
if not contact:
|
contact = frappe.get_doc('Contact', contact)
|
||||||
contact_name = get_default_contact(self.doctype, self.name)
|
|
||||||
if contact_name:
|
if not contact.has_link(self.doctype, self.name):
|
||||||
contact = frappe.get_doc('Contact', contact_name)
|
contact.append('links', dict(link_doctype=self.doctype, link_name=self.name))
|
||||||
|
|
||||||
if contact:
|
|
||||||
if self.email and self.email != contact.email_id:
|
if self.email and self.email != contact.email_id:
|
||||||
for email in contact.email_ids:
|
for email in contact.email_ids:
|
||||||
email.is_primary = True if email.email_id == self.email else False
|
email.is_primary = True if email.email_id == self.email else False
|
||||||
@@ -206,7 +221,7 @@ class Patient(Document):
|
|||||||
contact.add_phone(self.phone, is_primary_phone=True)
|
contact.add_phone(self.phone, is_primary_phone=True)
|
||||||
contact.set_primary('phone')
|
contact.set_primary('phone')
|
||||||
|
|
||||||
contact.flags.ignore_validate = True # disable hook TODO: safe?
|
contact.flags.skip_patient_update = True
|
||||||
contact.save(ignore_permissions=True)
|
contact.save(ignore_permissions=True)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -35,3 +35,40 @@ class TestPatient(unittest.TestCase):
|
|||||||
|
|
||||||
settings.collect_registration_fee = 0
|
settings.collect_registration_fee = 0
|
||||||
settings.save()
|
settings.save()
|
||||||
|
|
||||||
|
def test_patient_contact(self):
|
||||||
|
frappe.db.sql("""delete from `tabPatient` where name like '_Test Patient%'""")
|
||||||
|
frappe.db.sql("""delete from `tabCustomer` where name like '_Test Patient%'""")
|
||||||
|
frappe.db.sql("""delete from `tabContact` where name like'_Test Patient%'""")
|
||||||
|
frappe.db.sql("""delete from `tabDynamic Link` where parent like '_Test Patient%'""")
|
||||||
|
|
||||||
|
patient = create_patient(patient_name='_Test Patient Contact', email='test-patient@example.com', mobile='+91 0000000001')
|
||||||
|
customer = frappe.db.get_value('Patient', patient, 'customer')
|
||||||
|
self.assertTrue(customer)
|
||||||
|
self.assertTrue(frappe.db.exists('Dynamic Link', {'parenttype': 'Contact', 'link_doctype': 'Patient', 'link_name': patient}))
|
||||||
|
self.assertTrue(frappe.db.exists('Dynamic Link', {'parenttype': 'Contact', 'link_doctype': 'Customer', 'link_name': customer}))
|
||||||
|
|
||||||
|
# a second patient linking with same customer
|
||||||
|
new_patient = create_patient(email='test-patient@example.com', mobile='+91 0000000009', customer=customer)
|
||||||
|
self.assertTrue(frappe.db.exists('Dynamic Link', {'parenttype': 'Contact', 'link_doctype': 'Patient', 'link_name': new_patient}))
|
||||||
|
self.assertTrue(frappe.db.exists('Dynamic Link', {'parenttype': 'Contact', 'link_doctype': 'Customer', 'link_name': customer}))
|
||||||
|
|
||||||
|
def test_patient_user(self):
|
||||||
|
frappe.db.sql("""delete from `tabUser` where email='test-patient-user@example.com'""")
|
||||||
|
frappe.db.sql("""delete from `tabDynamic Link` where parent like '_Test Patient%'""")
|
||||||
|
frappe.db.sql("""delete from `tabPatient` where name like '_Test Patient%'""")
|
||||||
|
|
||||||
|
patient = create_patient(patient_name='_Test Patient User', email='test-patient-user@example.com', mobile='+91 0000000009', create_user=True)
|
||||||
|
user = frappe.db.get_value('Patient', patient, 'user_id')
|
||||||
|
self.assertTrue(frappe.db.exists('User', user))
|
||||||
|
|
||||||
|
new_patient = frappe.get_doc({
|
||||||
|
'doctype': 'Patient',
|
||||||
|
'first_name': '_Test Patient Duplicate User',
|
||||||
|
'sex': 'Male',
|
||||||
|
'email': 'test-patient-user@example.com',
|
||||||
|
'mobile': '+91 0000000009',
|
||||||
|
'invite_user': 1
|
||||||
|
})
|
||||||
|
|
||||||
|
self.assertRaises(frappe.exceptions.DuplicateEntryError, new_patient.insert)
|
||||||
|
|||||||
@@ -307,14 +307,18 @@ def create_healthcare_docs(id=0):
|
|||||||
return patient, practitioner
|
return patient, practitioner
|
||||||
|
|
||||||
|
|
||||||
def create_patient(id=0):
|
def create_patient(id=0, patient_name=None, email=None, mobile=None, customer=None, create_user=False):
|
||||||
if frappe.db.exists('Patient', {'firstname':f'_Test Patient {str(id)}'}):
|
if frappe.db.exists('Patient', {'firstname':f'_Test Patient {str(id)}'}):
|
||||||
patient = frappe.db.get_value('Patient', {'first_name': f'_Test Patient {str(id)}'}, ['name'])
|
patient = frappe.db.get_value('Patient', {'first_name': f'_Test Patient {str(id)}'}, ['name'])
|
||||||
return patient
|
return patient
|
||||||
|
|
||||||
patient = frappe.new_doc('Patient')
|
patient = frappe.new_doc('Patient')
|
||||||
patient.first_name = f'_Test Patient {str(id)}'
|
patient.first_name = patient_name if patient_name else f'_Test Patient {str(id)}'
|
||||||
patient.sex = 'Female'
|
patient.sex = 'Female'
|
||||||
|
patient.mobile = mobile
|
||||||
|
patient.email = email
|
||||||
|
patient.customer = customer
|
||||||
|
patient.invite_user = create_user
|
||||||
patient.save(ignore_permissions=True)
|
patient.save(ignore_permissions=True)
|
||||||
|
|
||||||
return patient.name
|
return patient.name
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ from __future__ import unicode_literals
|
|||||||
import unittest
|
import unittest
|
||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
from frappe.utils import nowdate
|
from frappe.utils import add_days, nowdate
|
||||||
|
|
||||||
from erpnext.accounts.doctype.pos_profile.test_pos_profile import make_pos_profile
|
from erpnext.accounts.doctype.pos_profile.test_pos_profile import make_pos_profile
|
||||||
from erpnext.healthcare.doctype.patient_appointment.test_patient_appointment import (
|
from erpnext.healthcare.doctype.patient_appointment.test_patient_appointment import (
|
||||||
@@ -38,7 +38,7 @@ class TestPatientMedicalRecord(unittest.TestCase):
|
|||||||
medical_rec = frappe.db.exists('Patient Medical Record', {'status': 'Open', 'reference_name': vital_signs.name})
|
medical_rec = frappe.db.exists('Patient Medical Record', {'status': 'Open', 'reference_name': vital_signs.name})
|
||||||
self.assertTrue(medical_rec)
|
self.assertTrue(medical_rec)
|
||||||
|
|
||||||
appointment = create_appointment(patient, practitioner, nowdate(), invoice=1, procedure_template=1)
|
appointment = create_appointment(patient, practitioner, add_days(nowdate(), 1), invoice=1, procedure_template=1)
|
||||||
procedure = create_procedure(appointment)
|
procedure = create_procedure(appointment)
|
||||||
procedure.start_procedure()
|
procedure.start_procedure()
|
||||||
procedure.complete_procedure()
|
procedure.complete_procedure()
|
||||||
|
|||||||
@@ -776,7 +776,7 @@ def update_patient_email_and_phone_numbers(contact, method):
|
|||||||
Hook validate Contact
|
Hook validate Contact
|
||||||
Update linked Patients' primary mobile and phone numbers
|
Update linked Patients' primary mobile and phone numbers
|
||||||
'''
|
'''
|
||||||
if 'Healthcare' not in frappe.get_active_domains():
|
if 'Healthcare' not in frappe.get_active_domains() or contact.flags.skip_patient_update:
|
||||||
return
|
return
|
||||||
|
|
||||||
if contact.is_primary_contact and (contact.email_id or contact.mobile_no or contact.phone):
|
if contact.is_primary_contact and (contact.email_id or contact.mobile_no or contact.phone):
|
||||||
@@ -784,9 +784,15 @@ def update_patient_email_and_phone_numbers(contact, method):
|
|||||||
|
|
||||||
for link in patient_links:
|
for link in patient_links:
|
||||||
contact_details = frappe.db.get_value('Patient', link.get('link_name'), ['email', 'mobile', 'phone'], as_dict=1)
|
contact_details = frappe.db.get_value('Patient', link.get('link_name'), ['email', 'mobile', 'phone'], as_dict=1)
|
||||||
|
|
||||||
|
new_contact_details = {}
|
||||||
|
|
||||||
if contact.email_id and contact.email_id != contact_details.get('email'):
|
if contact.email_id and contact.email_id != contact_details.get('email'):
|
||||||
frappe.db.set_value('Patient', link.get('link_name'), 'email', contact.email_id)
|
new_contact_details.update({'email': contact.email_id})
|
||||||
if contact.mobile_no and contact.mobile_no != contact_details.get('mobile'):
|
if contact.mobile_no and contact.mobile_no != contact_details.get('mobile'):
|
||||||
frappe.db.set_value('Patient', link.get('link_name'), 'mobile', contact.mobile_no)
|
new_contact_details.update({'mobile': contact.mobile_no})
|
||||||
if contact.phone and contact.phone != contact_details.get('phone'):
|
if contact.phone and contact.phone != contact_details.get('phone'):
|
||||||
frappe.db.set_value('Patient', link.get('link_name'), 'phone', contact.phone)
|
new_contact_details.update({'phone': contact.phone})
|
||||||
|
|
||||||
|
if new_contact_details:
|
||||||
|
frappe.db.set_value('Patient', link.get('link_name'), new_contact_details)
|
||||||
|
|||||||
@@ -61,6 +61,7 @@ treeviews = ['Account', 'Cost Center', 'Warehouse', 'Item Group', 'Customer Grou
|
|||||||
# website
|
# website
|
||||||
update_website_context = ["erpnext.e_commerce.shopping_cart.utils.update_website_context", "erpnext.education.doctype.education_settings.education_settings.update_website_context"]
|
update_website_context = ["erpnext.e_commerce.shopping_cart.utils.update_website_context", "erpnext.education.doctype.education_settings.education_settings.update_website_context"]
|
||||||
my_account_context = "erpnext.e_commerce.shopping_cart.utils.update_my_account_context"
|
my_account_context = "erpnext.e_commerce.shopping_cart.utils.update_my_account_context"
|
||||||
|
webform_list_context = "erpnext.controllers.website_list_for_contact.get_webform_list_context"
|
||||||
|
|
||||||
calendars = ["Task", "Work Order", "Leave Application", "Sales Order", "Holiday List", "Course Schedule"]
|
calendars = ["Task", "Work Order", "Leave Application", "Sales Order", "Holiday List", "Course Schedule"]
|
||||||
|
|
||||||
@@ -436,7 +437,7 @@ accounting_dimension_doctypes = ["GL Entry", "Sales Invoice", "Purchase Invoice"
|
|||||||
"Purchase Receipt Item", "Stock Entry Detail", "Payment Entry Deduction", "Sales Taxes and Charges", "Purchase Taxes and Charges", "Shipping Rule",
|
"Purchase Receipt Item", "Stock Entry Detail", "Payment Entry Deduction", "Sales Taxes and Charges", "Purchase Taxes and Charges", "Shipping Rule",
|
||||||
"Landed Cost Item", "Asset Value Adjustment", "Loyalty Program", "Fee Schedule", "Fee Structure", "Stock Reconciliation",
|
"Landed Cost Item", "Asset Value Adjustment", "Loyalty Program", "Fee Schedule", "Fee Structure", "Stock Reconciliation",
|
||||||
"Travel Request", "Fees", "POS Profile", "Opening Invoice Creation Tool", "Opening Invoice Creation Tool Item", "Subscription",
|
"Travel Request", "Fees", "POS Profile", "Opening Invoice Creation Tool", "Opening Invoice Creation Tool Item", "Subscription",
|
||||||
"Subscription Plan"
|
"Subscription Plan", "POS Invoice", "POS Invoice Item"
|
||||||
]
|
]
|
||||||
|
|
||||||
regional_overrides = {
|
regional_overrides = {
|
||||||
|
|||||||
@@ -184,7 +184,7 @@ def get_employees_having_an_event_today(event_type):
|
|||||||
# --------------------------
|
# --------------------------
|
||||||
def send_work_anniversary_reminders():
|
def send_work_anniversary_reminders():
|
||||||
"""Send Employee Work Anniversary Reminders if 'Send Work Anniversary Reminders' is checked"""
|
"""Send Employee Work Anniversary Reminders if 'Send Work Anniversary Reminders' is checked"""
|
||||||
to_send = int(frappe.db.get_single_value("HR Settings", "send_work_anniversary_reminders") or 1)
|
to_send = int(frappe.db.get_single_value("HR Settings", "send_work_anniversary_reminders"))
|
||||||
if not to_send:
|
if not to_send:
|
||||||
return
|
return
|
||||||
|
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ frappe.ui.form.on('Training Result', {
|
|||||||
frm.set_value("employees" ,"");
|
frm.set_value("employees" ,"");
|
||||||
if (r.message) {
|
if (r.message) {
|
||||||
$.each(r.message, function(i, d) {
|
$.each(r.message, function(i, d) {
|
||||||
var row = frappe.model.add_child(cur_frm.doc, "Training Result Employee", "employees");
|
var row = frappe.model.add_child(frm.doc, "Training Result Employee", "employees");
|
||||||
row.employee = d.employee;
|
row.employee = d.employee;
|
||||||
row.employee_name = d.employee_name;
|
row.employee_name = d.employee_name;
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ frappe.ui.form.on('Maintenance Schedule', {
|
|||||||
},
|
},
|
||||||
refresh: function (frm) {
|
refresh: function (frm) {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
frm.toggle_display('generate_schedule', !(frm.is_new()));
|
frm.toggle_display('generate_schedule', !(frm.is_new() || frm.doc.docstatus));
|
||||||
frm.toggle_display('schedule', !(frm.is_new()));
|
frm.toggle_display('schedule', !(frm.is_new()));
|
||||||
}, 10);
|
}, 10);
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -16,9 +16,9 @@ from erpnext.utilities.transaction_base import TransactionBase, delete_events
|
|||||||
class MaintenanceSchedule(TransactionBase):
|
class MaintenanceSchedule(TransactionBase):
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def generate_schedule(self):
|
def generate_schedule(self):
|
||||||
|
if self.docstatus != 0:
|
||||||
|
return
|
||||||
self.set('schedules', [])
|
self.set('schedules', [])
|
||||||
frappe.db.sql("""delete from `tabMaintenance Schedule Detail`
|
|
||||||
where parent=%s""", (self.name))
|
|
||||||
count = 1
|
count = 1
|
||||||
for d in self.get('items'):
|
for d in self.get('items'):
|
||||||
self.validate_maintenance_detail()
|
self.validate_maintenance_detail()
|
||||||
|
|||||||
@@ -1135,7 +1135,8 @@ def item_query(doctype, txt, searchfield, start, page_len, filters):
|
|||||||
query_filters["has_variants"] = 0
|
query_filters["has_variants"] = 0
|
||||||
|
|
||||||
if filters and filters.get("is_stock_item"):
|
if filters and filters.get("is_stock_item"):
|
||||||
query_filters["is_stock_item"] = 1
|
or_cond_filters["is_stock_item"] = 1
|
||||||
|
or_cond_filters["has_variants"] = 1
|
||||||
|
|
||||||
return frappe.get_list("Item",
|
return frappe.get_list("Item",
|
||||||
fields = fields, filters=query_filters,
|
fields = fields, filters=query_filters,
|
||||||
|
|||||||
@@ -38,6 +38,8 @@
|
|||||||
"total_time_in_mins",
|
"total_time_in_mins",
|
||||||
"section_break_8",
|
"section_break_8",
|
||||||
"items",
|
"items",
|
||||||
|
"scrap_items_section",
|
||||||
|
"scrap_items",
|
||||||
"corrective_operation_section",
|
"corrective_operation_section",
|
||||||
"for_job_card",
|
"for_job_card",
|
||||||
"is_corrective_job_card",
|
"is_corrective_job_card",
|
||||||
@@ -392,11 +394,24 @@
|
|||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"label": "Batch No",
|
"label": "Batch No",
|
||||||
"options": "Batch"
|
"options": "Batch"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "scrap_items_section",
|
||||||
|
"fieldtype": "Section Break",
|
||||||
|
"label": "Scrap Items"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "scrap_items",
|
||||||
|
"fieldtype": "Table",
|
||||||
|
"label": "Scrap Items",
|
||||||
|
"no_copy": 1,
|
||||||
|
"options": "Job Card Scrap Item",
|
||||||
|
"print_hide": 1
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"is_submittable": 1,
|
"is_submittable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2021-09-13 21:34:15.177928",
|
"modified": "2021-09-14 00:38:46.873105",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Manufacturing",
|
"module": "Manufacturing",
|
||||||
"name": "Job Card",
|
"name": "Job Card",
|
||||||
|
|||||||
@@ -677,7 +677,7 @@ def get_job_details(start, end, filters=None):
|
|||||||
conditions = get_filters_cond("Job Card", filters, [])
|
conditions = get_filters_cond("Job Card", filters, [])
|
||||||
|
|
||||||
job_cards = frappe.db.sql(""" SELECT `tabJob Card`.name, `tabJob Card`.work_order,
|
job_cards = frappe.db.sql(""" SELECT `tabJob Card`.name, `tabJob Card`.work_order,
|
||||||
`tabJob Card`.employee_name, `tabJob Card`.status, ifnull(`tabJob Card`.remarks, ''),
|
`tabJob Card`.status, ifnull(`tabJob Card`.remarks, ''),
|
||||||
min(`tabJob Card Time Log`.from_time) as from_time,
|
min(`tabJob Card Time Log`.from_time) as from_time,
|
||||||
max(`tabJob Card Time Log`.to_time) as to_time
|
max(`tabJob Card Time Log`.to_time) as to_time
|
||||||
FROM `tabJob Card` , `tabJob Card Time Log`
|
FROM `tabJob Card` , `tabJob Card Time Log`
|
||||||
@@ -687,7 +687,7 @@ def get_job_details(start, end, filters=None):
|
|||||||
|
|
||||||
for d in job_cards:
|
for d in job_cards:
|
||||||
subject_data = []
|
subject_data = []
|
||||||
for field in ["name", "work_order", "remarks", "employee_name"]:
|
for field in ["name", "work_order", "remarks"]:
|
||||||
if not d.get(field): continue
|
if not d.get(field): continue
|
||||||
|
|
||||||
subject_data.append(d.get(field))
|
subject_data.append(d.get(field))
|
||||||
|
|||||||
@@ -0,0 +1,82 @@
|
|||||||
|
{
|
||||||
|
"actions": [],
|
||||||
|
"creation": "2021-09-14 00:30:28.533884",
|
||||||
|
"doctype": "DocType",
|
||||||
|
"editable_grid": 1,
|
||||||
|
"engine": "InnoDB",
|
||||||
|
"field_order": [
|
||||||
|
"item_code",
|
||||||
|
"item_name",
|
||||||
|
"column_break_3",
|
||||||
|
"description",
|
||||||
|
"quantity_and_rate",
|
||||||
|
"stock_qty",
|
||||||
|
"column_break_6",
|
||||||
|
"stock_uom"
|
||||||
|
],
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldname": "item_code",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"in_list_view": 1,
|
||||||
|
"label": "Scrap Item Code",
|
||||||
|
"options": "Item",
|
||||||
|
"reqd": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fetch_from": "item_code.item_name",
|
||||||
|
"fieldname": "item_name",
|
||||||
|
"fieldtype": "Data",
|
||||||
|
"in_list_view": 1,
|
||||||
|
"label": "Scrap Item Name"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "column_break_3",
|
||||||
|
"fieldtype": "Column Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fetch_from": "item_code.description",
|
||||||
|
"fieldname": "description",
|
||||||
|
"fieldtype": "Small Text",
|
||||||
|
"label": "Description",
|
||||||
|
"read_only": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "quantity_and_rate",
|
||||||
|
"fieldtype": "Section Break",
|
||||||
|
"label": "Quantity and Rate"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "stock_qty",
|
||||||
|
"fieldtype": "Float",
|
||||||
|
"in_list_view": 1,
|
||||||
|
"label": "Qty",
|
||||||
|
"reqd": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "column_break_6",
|
||||||
|
"fieldtype": "Column Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fetch_from": "item_code.stock_uom",
|
||||||
|
"fieldname": "stock_uom",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"label": "Stock UOM",
|
||||||
|
"options": "UOM",
|
||||||
|
"read_only": 1
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"index_web_pages_for_search": 1,
|
||||||
|
"istable": 1,
|
||||||
|
"links": [],
|
||||||
|
"modified": "2021-09-14 01:20:48.588052",
|
||||||
|
"modified_by": "Administrator",
|
||||||
|
"module": "Manufacturing",
|
||||||
|
"name": "Job Card Scrap Item",
|
||||||
|
"owner": "Administrator",
|
||||||
|
"permissions": [],
|
||||||
|
"quick_entry": 1,
|
||||||
|
"sort_field": "modified",
|
||||||
|
"sort_order": "DESC",
|
||||||
|
"track_changes": 1
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
|
||||||
|
# For license information, please see license.txt
|
||||||
|
|
||||||
|
from frappe.model.document import Document
|
||||||
|
|
||||||
|
|
||||||
|
class JobCardScrapItem(Document):
|
||||||
|
pass
|
||||||
@@ -242,6 +242,8 @@ frappe.ui.form.on('Production Plan', {
|
|||||||
},
|
},
|
||||||
|
|
||||||
get_sub_assembly_items: function(frm) {
|
get_sub_assembly_items: function(frm) {
|
||||||
|
frm.dirty();
|
||||||
|
|
||||||
frappe.call({
|
frappe.call({
|
||||||
method: "get_sub_assembly_items",
|
method: "get_sub_assembly_items",
|
||||||
freeze: true,
|
freeze: true,
|
||||||
|
|||||||
@@ -457,7 +457,8 @@ class ProductionPlan(Document):
|
|||||||
|
|
||||||
def prepare_args_for_sub_assembly_items(self, row, args):
|
def prepare_args_for_sub_assembly_items(self, row, args):
|
||||||
for field in ["production_item", "item_name", "qty", "fg_warehouse",
|
for field in ["production_item", "item_name", "qty", "fg_warehouse",
|
||||||
"description", "bom_no", "stock_uom", "bom_level", "production_plan_item"]:
|
"description", "bom_no", "stock_uom", "bom_level",
|
||||||
|
"production_plan_item", "schedule_date"]:
|
||||||
args[field] = row.get(field)
|
args[field] = row.get(field)
|
||||||
|
|
||||||
args.update({
|
args.update({
|
||||||
@@ -561,8 +562,6 @@ class ProductionPlan(Document):
|
|||||||
get_sub_assembly_items(row.bom_no, bom_data, row.planned_qty)
|
get_sub_assembly_items(row.bom_no, bom_data, row.planned_qty)
|
||||||
self.set_sub_assembly_items_based_on_level(row, bom_data, manufacturing_type)
|
self.set_sub_assembly_items_based_on_level(row, bom_data, manufacturing_type)
|
||||||
|
|
||||||
self.save()
|
|
||||||
|
|
||||||
def set_sub_assembly_items_based_on_level(self, row, bom_data, manufacturing_type=None):
|
def set_sub_assembly_items_based_on_level(self, row, bom_data, manufacturing_type=None):
|
||||||
bom_data = sorted(bom_data, key = lambda i: i.bom_level)
|
bom_data = sorted(bom_data, key = lambda i: i.bom_level)
|
||||||
|
|
||||||
|
|||||||
@@ -404,6 +404,7 @@ def make_bom(**args):
|
|||||||
'uom': item_doc.stock_uom,
|
'uom': item_doc.stock_uom,
|
||||||
'stock_uom': item_doc.stock_uom,
|
'stock_uom': item_doc.stock_uom,
|
||||||
'rate': item_doc.valuation_rate or args.rate,
|
'rate': item_doc.valuation_rate or args.rate,
|
||||||
|
'source_warehouse': args.source_warehouse
|
||||||
})
|
})
|
||||||
|
|
||||||
if not args.do_not_save:
|
if not args.do_not_save:
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ from erpnext.manufacturing.doctype.work_order.work_order import (
|
|||||||
stop_unstop,
|
stop_unstop,
|
||||||
)
|
)
|
||||||
from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order
|
from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order
|
||||||
from erpnext.stock.doctype.item.test_item import make_item
|
from erpnext.stock.doctype.item.test_item import create_item, make_item
|
||||||
from erpnext.stock.doctype.stock_entry import test_stock_entry
|
from erpnext.stock.doctype.stock_entry import test_stock_entry
|
||||||
from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse
|
from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse
|
||||||
from erpnext.stock.utils import get_bin
|
from erpnext.stock.utils import get_bin
|
||||||
@@ -768,6 +768,60 @@ class TestWorkOrder(unittest.TestCase):
|
|||||||
total_pl_qty
|
total_pl_qty
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def test_job_card_scrap_item(self):
|
||||||
|
items = ['Test FG Item for Scrap Item Test', 'Test RM Item 1 for Scrap Item Test',
|
||||||
|
'Test RM Item 2 for Scrap Item Test']
|
||||||
|
|
||||||
|
company = '_Test Company with perpetual inventory'
|
||||||
|
for item_code in items:
|
||||||
|
create_item(item_code = item_code, is_stock_item = 1,
|
||||||
|
is_purchase_item=1, opening_stock=100, valuation_rate=10, company=company, warehouse='Stores - TCP1')
|
||||||
|
|
||||||
|
item = 'Test FG Item for Scrap Item Test'
|
||||||
|
raw_materials = ['Test RM Item 1 for Scrap Item Test', 'Test RM Item 2 for Scrap Item Test']
|
||||||
|
if not frappe.db.get_value('BOM', {'item': item}):
|
||||||
|
bom = make_bom(item=item, source_warehouse='Stores - TCP1', raw_materials=raw_materials, do_not_save=True)
|
||||||
|
bom.with_operations = 1
|
||||||
|
bom.append('operations', {
|
||||||
|
'operation': '_Test Operation 1',
|
||||||
|
'workstation': '_Test Workstation 1',
|
||||||
|
'hour_rate': 20,
|
||||||
|
'time_in_mins': 60
|
||||||
|
})
|
||||||
|
|
||||||
|
bom.submit()
|
||||||
|
|
||||||
|
wo_order = make_wo_order_test_record(item=item, company=company, planned_start_date=now(), qty=20, skip_transfer=1)
|
||||||
|
job_card = frappe.db.get_value('Job Card', {'work_order': wo_order.name}, 'name')
|
||||||
|
update_job_card(job_card)
|
||||||
|
|
||||||
|
stock_entry = frappe.get_doc(make_stock_entry(wo_order.name, "Manufacture", 10))
|
||||||
|
for row in stock_entry.items:
|
||||||
|
if row.is_scrap_item:
|
||||||
|
self.assertEqual(row.qty, 1)
|
||||||
|
|
||||||
|
def update_job_card(job_card):
|
||||||
|
job_card_doc = frappe.get_doc('Job Card', job_card)
|
||||||
|
job_card_doc.set('scrap_items', [
|
||||||
|
{
|
||||||
|
'item_code': 'Test RM Item 1 for Scrap Item Test',
|
||||||
|
'stock_qty': 2
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'item_code': 'Test RM Item 2 for Scrap Item Test',
|
||||||
|
'stock_qty': 2
|
||||||
|
},
|
||||||
|
])
|
||||||
|
|
||||||
|
job_card_doc.append('time_logs', {
|
||||||
|
'from_time': now(),
|
||||||
|
'time_in_mins': 60,
|
||||||
|
'completed_qty': job_card_doc.for_quantity
|
||||||
|
})
|
||||||
|
|
||||||
|
job_card_doc.submit()
|
||||||
|
|
||||||
|
|
||||||
def get_scrap_item_details(bom_no):
|
def get_scrap_item_details(bom_no):
|
||||||
scrap_items = {}
|
scrap_items = {}
|
||||||
for item in frappe.db.sql("""select item_code, stock_qty from `tabBOM Scrap Item`
|
for item in frappe.db.sql("""select item_code, stock_qty from `tabBOM Scrap Item`
|
||||||
|
|||||||
@@ -313,3 +313,7 @@ erpnext.patches.v13_0.create_website_items
|
|||||||
erpnext.patches.v13_0.populate_e_commerce_settings
|
erpnext.patches.v13_0.populate_e_commerce_settings
|
||||||
erpnext.patches.v13_0.make_homepage_products_website_items
|
erpnext.patches.v13_0.make_homepage_products_website_items
|
||||||
erpnext.patches.v13_0.update_dates_in_tax_withholding_category
|
erpnext.patches.v13_0.update_dates_in_tax_withholding_category
|
||||||
|
erpnext.patches.v13_0.replace_supplier_item_group_with_party_specific_item
|
||||||
|
erpnext.patches.v13_0.create_accounting_dimensions_in_pos_doctypes
|
||||||
|
erpnext.patches.v13_0.create_custom_field_for_finance_book
|
||||||
|
erpnext.patches.v13_0.modify_invalid_gain_loss_gl_entries
|
||||||
|
|||||||
@@ -0,0 +1,42 @@
|
|||||||
|
import frappe
|
||||||
|
from frappe.custom.doctype.custom_field.custom_field import create_custom_field
|
||||||
|
|
||||||
|
|
||||||
|
def execute():
|
||||||
|
frappe.reload_doc('accounts', 'doctype', 'accounting_dimension')
|
||||||
|
accounting_dimensions = frappe.db.sql("""select fieldname, label, document_type, disabled from
|
||||||
|
`tabAccounting Dimension`""", as_dict=1)
|
||||||
|
|
||||||
|
if not accounting_dimensions:
|
||||||
|
return
|
||||||
|
|
||||||
|
count = 1
|
||||||
|
for d in accounting_dimensions:
|
||||||
|
|
||||||
|
if count % 2 == 0:
|
||||||
|
insert_after_field = 'dimension_col_break'
|
||||||
|
else:
|
||||||
|
insert_after_field = 'accounting_dimensions_section'
|
||||||
|
|
||||||
|
for doctype in ["POS Invoice", "POS Invoice Item"]:
|
||||||
|
|
||||||
|
field = frappe.db.get_value("Custom Field", {"dt": doctype, "fieldname": d.fieldname})
|
||||||
|
|
||||||
|
if field:
|
||||||
|
continue
|
||||||
|
meta = frappe.get_meta(doctype, cached=False)
|
||||||
|
fieldnames = [d.fieldname for d in meta.get("fields")]
|
||||||
|
|
||||||
|
df = {
|
||||||
|
"fieldname": d.fieldname,
|
||||||
|
"label": d.label,
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"options": d.document_type,
|
||||||
|
"insert_after": insert_after_field
|
||||||
|
}
|
||||||
|
|
||||||
|
if df['fieldname'] not in fieldnames:
|
||||||
|
create_custom_field(doctype, df)
|
||||||
|
frappe.clear_cache(doctype=doctype)
|
||||||
|
|
||||||
|
count += 1
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import frappe
|
||||||
|
from frappe.custom.doctype.custom_field.custom_field import create_custom_fields
|
||||||
|
|
||||||
|
|
||||||
|
def execute():
|
||||||
|
company = frappe.get_all('Company', filters = {'country': 'India'})
|
||||||
|
if not company:
|
||||||
|
return
|
||||||
|
|
||||||
|
custom_field = {
|
||||||
|
'Finance Book': [
|
||||||
|
{
|
||||||
|
'fieldname': 'for_income_tax',
|
||||||
|
'label': 'For Income Tax',
|
||||||
|
'fieldtype': 'Check',
|
||||||
|
'insert_after': 'finance_book_name',
|
||||||
|
'description': 'If the asset is put to use for less than 180 days, the first Depreciation Rate will be reduced by 50%.'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
create_custom_fields(custom_field, update=1)
|
||||||
49
erpnext/patches/v13_0/modify_invalid_gain_loss_gl_entries.py
Normal file
49
erpnext/patches/v13_0/modify_invalid_gain_loss_gl_entries.py
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
import json
|
||||||
|
|
||||||
|
import frappe
|
||||||
|
|
||||||
|
|
||||||
|
def execute():
|
||||||
|
frappe.reload_doc('accounts', 'doctype', 'purchase_invoice_advance')
|
||||||
|
frappe.reload_doc('accounts', 'doctype', 'sales_invoice_advance')
|
||||||
|
|
||||||
|
purchase_invoices = frappe.db.sql("""
|
||||||
|
select
|
||||||
|
parenttype as type, parent as name
|
||||||
|
from
|
||||||
|
`tabPurchase Invoice Advance`
|
||||||
|
where
|
||||||
|
ref_exchange_rate = 1
|
||||||
|
and docstatus = 1
|
||||||
|
and ifnull(exchange_gain_loss, '') != ''
|
||||||
|
group by
|
||||||
|
parent
|
||||||
|
""", as_dict=1)
|
||||||
|
|
||||||
|
sales_invoices = frappe.db.sql("""
|
||||||
|
select
|
||||||
|
parenttype as type, parent as name
|
||||||
|
from
|
||||||
|
`tabSales Invoice Advance`
|
||||||
|
where
|
||||||
|
ref_exchange_rate = 1
|
||||||
|
and docstatus = 1
|
||||||
|
and ifnull(exchange_gain_loss, '') != ''
|
||||||
|
group by
|
||||||
|
parent
|
||||||
|
""", as_dict=1)
|
||||||
|
|
||||||
|
if purchase_invoices + sales_invoices:
|
||||||
|
frappe.log_error(json.dumps(purchase_invoices + sales_invoices, indent=2), title="Patch Log")
|
||||||
|
|
||||||
|
for invoice in purchase_invoices + sales_invoices:
|
||||||
|
doc = frappe.get_doc(invoice.type, invoice.name)
|
||||||
|
doc.docstatus = 2
|
||||||
|
doc.make_gl_entries()
|
||||||
|
for advance in doc.advances:
|
||||||
|
if advance.ref_exchange_rate == 1:
|
||||||
|
advance.db_set('exchange_gain_loss', 0, False)
|
||||||
|
doc.docstatus = 1
|
||||||
|
doc.make_gl_entries()
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
# Copyright (c) 2019, Frappe and Contributors
|
||||||
|
# License: GNU General Public License v3. See license.txt
|
||||||
|
|
||||||
|
import frappe
|
||||||
|
|
||||||
|
|
||||||
|
def execute():
|
||||||
|
if frappe.db.table_exists('Supplier Item Group'):
|
||||||
|
frappe.reload_doc("selling", "doctype", "party_specific_item")
|
||||||
|
sig = frappe.db.get_all("Supplier Item Group", fields=["name", "supplier", "item_group"])
|
||||||
|
for item in sig:
|
||||||
|
psi = frappe.new_doc("Party Specific Item")
|
||||||
|
psi.party_type = "Supplier"
|
||||||
|
psi.party = item.supplier
|
||||||
|
psi.restrict_based_on = "Item Group"
|
||||||
|
psi.based_on_value = item.item_group
|
||||||
|
psi.insert()
|
||||||
@@ -141,6 +141,9 @@ class Project(Document):
|
|||||||
if self.sales_order:
|
if self.sales_order:
|
||||||
frappe.db.set_value("Sales Order", self.sales_order, "project", self.name)
|
frappe.db.set_value("Sales Order", self.sales_order, "project", self.name)
|
||||||
|
|
||||||
|
def on_trash(self):
|
||||||
|
frappe.db.set_value("Sales Order", {"project": self.name}, "project", "")
|
||||||
|
|
||||||
def update_percent_complete(self):
|
def update_percent_complete(self):
|
||||||
if self.percent_complete_method == "Manual":
|
if self.percent_complete_method == "Manual":
|
||||||
if self.status == "Completed":
|
if self.status == "Completed":
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ from frappe.utils import add_days, getdate, nowdate
|
|||||||
|
|
||||||
from erpnext.projects.doctype.project_template.test_project_template import make_project_template
|
from erpnext.projects.doctype.project_template.test_project_template import make_project_template
|
||||||
from erpnext.projects.doctype.task.test_task import create_task
|
from erpnext.projects.doctype.task.test_task import create_task
|
||||||
|
from erpnext.selling.doctype.sales_order.sales_order import make_project as make_project_from_so
|
||||||
|
from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order
|
||||||
|
|
||||||
test_records = frappe.get_test_records('Project')
|
test_records = frappe.get_test_records('Project')
|
||||||
test_ignore = ["Sales Order"]
|
test_ignore = ["Sales Order"]
|
||||||
@@ -96,6 +98,21 @@ class TestProject(unittest.TestCase):
|
|||||||
|
|
||||||
self.assertEqual(len(tasks), 2)
|
self.assertEqual(len(tasks), 2)
|
||||||
|
|
||||||
|
def test_project_linking_with_sales_order(self):
|
||||||
|
so = make_sales_order()
|
||||||
|
project = make_project_from_so(so.name)
|
||||||
|
|
||||||
|
project.save()
|
||||||
|
self.assertEqual(project.sales_order, so.name)
|
||||||
|
|
||||||
|
so.reload()
|
||||||
|
self.assertEqual(so.project, project.name)
|
||||||
|
|
||||||
|
project.delete()
|
||||||
|
|
||||||
|
so.reload()
|
||||||
|
self.assertFalse(so.project)
|
||||||
|
|
||||||
def get_project(name, template):
|
def get_project(name, template):
|
||||||
|
|
||||||
project = frappe.get_doc(dict(
|
project = frappe.get_doc(dict(
|
||||||
|
|||||||
@@ -617,6 +617,7 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({
|
|||||||
me.frm.script_manager.trigger('qty', item.doctype, item.name);
|
me.frm.script_manager.trigger('qty', item.doctype, item.name);
|
||||||
if (!me.frm.doc.set_warehouse)
|
if (!me.frm.doc.set_warehouse)
|
||||||
me.frm.script_manager.trigger('warehouse', item.doctype, item.name);
|
me.frm.script_manager.trigger('warehouse', item.doctype, item.name);
|
||||||
|
me.apply_price_list(item, true);
|
||||||
}, undefined, !frappe.flags.hide_serial_batch_dialog);
|
}, undefined, !frappe.flags.hide_serial_batch_dialog);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -864,9 +865,11 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({
|
|||||||
if (r.message) {
|
if (r.message) {
|
||||||
me.frm.set_value("billing_address", r.message);
|
me.frm.set_value("billing_address", r.message);
|
||||||
} else {
|
} else {
|
||||||
|
if (frappe.meta.get_docfield(me.frm.doctype, 'company_address')) {
|
||||||
me.frm.set_value("company_address", "");
|
me.frm.set_value("company_address", "");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -105,6 +105,8 @@ $.extend(shopping_cart, {
|
|||||||
},
|
},
|
||||||
|
|
||||||
set_cart_count: function(animate=false) {
|
set_cart_count: function(animate=false) {
|
||||||
|
$(".intermediate-empty-cart").remove();
|
||||||
|
|
||||||
var cart_count = frappe.get_cookie("cart_count");
|
var cart_count = frappe.get_cookie("cart_count");
|
||||||
if(frappe.session.user==="Guest") {
|
if(frappe.session.user==="Guest") {
|
||||||
cart_count = 0;
|
cart_count = 0;
|
||||||
@@ -119,13 +121,20 @@ $.extend(shopping_cart, {
|
|||||||
|
|
||||||
if(parseInt(cart_count) === 0 || cart_count === undefined) {
|
if(parseInt(cart_count) === 0 || cart_count === undefined) {
|
||||||
$cart.css("display", "none");
|
$cart.css("display", "none");
|
||||||
$(".cart-items").html('Cart is Empty');
|
|
||||||
$(".cart-tax-items").hide();
|
$(".cart-tax-items").hide();
|
||||||
$(".btn-place-order").hide();
|
$(".btn-place-order").hide();
|
||||||
$(".cart-payment-addresses").hide();
|
$(".cart-payment-addresses").hide();
|
||||||
|
|
||||||
|
let intermediate_empty_cart_msg = `
|
||||||
|
<div class="text-center w-100 intermediate-empty-cart mt-4 mb-4 text-muted">
|
||||||
|
${ __("Cart is Empty") }
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
$(".cart-table").after(intermediate_empty_cart_msg);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
$cart.css("display", "inline");
|
$cart.css("display", "inline");
|
||||||
|
$("#cart-count").text(cart_count);
|
||||||
}
|
}
|
||||||
|
|
||||||
if(cart_count) {
|
if(cart_count) {
|
||||||
@@ -152,7 +161,10 @@ $.extend(shopping_cart, {
|
|||||||
callback: function(r) {
|
callback: function(r) {
|
||||||
if(!r.exc) {
|
if(!r.exc) {
|
||||||
$(".cart-items").html(r.message.items);
|
$(".cart-items").html(r.message.items);
|
||||||
$(".cart-tax-items").html(r.message.taxes);
|
$(".cart-tax-items").html(r.message.total);
|
||||||
|
$(".payment-summary").html(r.message.taxes_and_totals);
|
||||||
|
shopping_cart.set_cart_count();
|
||||||
|
|
||||||
if (cart_dropdown != true) {
|
if (cart_dropdown != true) {
|
||||||
$(".cart-icon").hide();
|
$(".cart-icon").hide();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -714,12 +714,15 @@ erpnext.utils.map_current_doc = function(opts) {
|
|||||||
child_columns: opts.child_columns,
|
child_columns: opts.child_columns,
|
||||||
action: function(selections, args) {
|
action: function(selections, args) {
|
||||||
let values = selections;
|
let values = selections;
|
||||||
if(values.length === 0){
|
if (values.length === 0) {
|
||||||
frappe.msgprint(__("Please select {0}", [opts.source_doctype]))
|
frappe.msgprint(__("Please select {0}", [opts.source_doctype]))
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
opts.source_name = values;
|
opts.source_name = values;
|
||||||
|
if (opts.allow_child_item_selection) {
|
||||||
|
// args contains filtered child docnames
|
||||||
opts.args = args;
|
opts.args = args;
|
||||||
|
}
|
||||||
d.dialog.hide();
|
d.dialog.hide();
|
||||||
_map();
|
_map();
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -706,6 +706,15 @@ def make_custom_fields(update=True):
|
|||||||
'fieldtype': 'Data',
|
'fieldtype': 'Data',
|
||||||
'insert_after': 'email'
|
'insert_after': 'email'
|
||||||
}
|
}
|
||||||
|
],
|
||||||
|
'Finance Book': [
|
||||||
|
{
|
||||||
|
'fieldname': 'for_income_tax',
|
||||||
|
'label': 'For Income Tax',
|
||||||
|
'fieldtype': 'Check',
|
||||||
|
'insert_after': 'finance_book_name',
|
||||||
|
'description': 'If the asset is put to use for less than 180 days, the first Depreciation Rate will be reduced by 50%.'
|
||||||
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
create_custom_fields(custom_fields, update=update)
|
create_custom_fields(custom_fields, update=update)
|
||||||
@@ -795,7 +804,7 @@ def set_salary_components(docs):
|
|||||||
|
|
||||||
def set_tax_withholding_category(company):
|
def set_tax_withholding_category(company):
|
||||||
accounts = []
|
accounts = []
|
||||||
fiscal_year = None
|
fiscal_year_details = None
|
||||||
abbr = frappe.get_value("Company", company, "abbr")
|
abbr = frappe.get_value("Company", company, "abbr")
|
||||||
tds_account = frappe.get_value("Account", 'TDS Payable - {0}'.format(abbr), 'name')
|
tds_account = frappe.get_value("Account", 'TDS Payable - {0}'.format(abbr), 'name')
|
||||||
|
|
||||||
|
|||||||
@@ -112,9 +112,6 @@ def validate_gstin_check_digit(gstin, label='GSTIN'):
|
|||||||
frappe.throw(_("""Invalid {0}! The check digit validation has failed. Please ensure you've typed the {0} correctly.""").format(label))
|
frappe.throw(_("""Invalid {0}! The check digit validation has failed. Please ensure you've typed the {0} correctly.""").format(label))
|
||||||
|
|
||||||
def get_itemised_tax_breakup_header(item_doctype, tax_accounts):
|
def get_itemised_tax_breakup_header(item_doctype, tax_accounts):
|
||||||
if frappe.get_meta(item_doctype).has_field('gst_hsn_code'):
|
|
||||||
return [_("HSN/SAC"), _("Taxable Amount")] + tax_accounts
|
|
||||||
else:
|
|
||||||
return [_("Item"), _("Taxable Amount")] + tax_accounts
|
return [_("Item"), _("Taxable Amount")] + tax_accounts
|
||||||
|
|
||||||
def get_itemised_tax_breakup_data(doc, account_wise=False):
|
def get_itemised_tax_breakup_data(doc, account_wise=False):
|
||||||
@@ -859,6 +856,7 @@ def get_depreciation_amount(asset, depreciable_value, row):
|
|||||||
rate_of_depreciation = row.rate_of_depreciation
|
rate_of_depreciation = row.rate_of_depreciation
|
||||||
# if its the first depreciation
|
# if its the first depreciation
|
||||||
if depreciable_value == asset.gross_purchase_amount:
|
if depreciable_value == asset.gross_purchase_amount:
|
||||||
|
if row.finance_book and frappe.db.get_value('Finance Book', row.finance_book, 'for_income_tax'):
|
||||||
# as per IT act, if the asset is purchased in the 2nd half of fiscal year, then rate is divided by 2
|
# as per IT act, if the asset is purchased in the 2nd half of fiscal year, then rate is divided by 2
|
||||||
diff = date_diff(row.depreciation_start_date, asset.available_for_use_date)
|
diff = date_diff(row.depreciation_start_date, asset.available_for_use_date)
|
||||||
if diff <= 180:
|
if diff <= 180:
|
||||||
|
|||||||
@@ -214,7 +214,7 @@ class Gstr1Report(object):
|
|||||||
|
|
||||||
|
|
||||||
if self.filters.get("type_of_business") == "B2B":
|
if self.filters.get("type_of_business") == "B2B":
|
||||||
conditions += "AND IFNULL(gst_category, '') in ('Registered Regular', 'Deemed Export', 'SEZ') AND is_return != 1"
|
conditions += "AND IFNULL(gst_category, '') in ('Registered Regular', 'Deemed Export', 'SEZ') AND is_return != 1 AND is_debit_note !=1"
|
||||||
|
|
||||||
if self.filters.get("type_of_business") in ("B2C Large", "B2C Small"):
|
if self.filters.get("type_of_business") in ("B2C Large", "B2C Small"):
|
||||||
b2c_limit = frappe.db.get_single_value('GST Settings', 'b2c_limit')
|
b2c_limit = frappe.db.get_single_value('GST Settings', 'b2c_limit')
|
||||||
@@ -223,7 +223,7 @@ class Gstr1Report(object):
|
|||||||
|
|
||||||
if self.filters.get("type_of_business") == "B2C Large":
|
if self.filters.get("type_of_business") == "B2C Large":
|
||||||
conditions += """ AND ifnull(SUBSTR(place_of_supply, 1, 2),'') != ifnull(SUBSTR(company_gstin, 1, 2),'')
|
conditions += """ AND ifnull(SUBSTR(place_of_supply, 1, 2),'') != ifnull(SUBSTR(company_gstin, 1, 2),'')
|
||||||
AND grand_total > {0} AND is_return != 1 and gst_category ='Unregistered' """.format(flt(b2c_limit))
|
AND grand_total > {0} AND is_return != 1 AND is_debit_note !=1 AND gst_category ='Unregistered' """.format(flt(b2c_limit))
|
||||||
|
|
||||||
elif self.filters.get("type_of_business") == "B2C Small":
|
elif self.filters.get("type_of_business") == "B2C Small":
|
||||||
conditions += """ AND (
|
conditions += """ AND (
|
||||||
@@ -236,8 +236,8 @@ class Gstr1Report(object):
|
|||||||
elif self.filters.get("type_of_business") == "CDNR-UNREG":
|
elif self.filters.get("type_of_business") == "CDNR-UNREG":
|
||||||
b2c_limit = frappe.db.get_single_value('GST Settings', 'b2c_limit')
|
b2c_limit = frappe.db.get_single_value('GST Settings', 'b2c_limit')
|
||||||
conditions += """ AND ifnull(SUBSTR(place_of_supply, 1, 2),'') != ifnull(SUBSTR(company_gstin, 1, 2),'')
|
conditions += """ AND ifnull(SUBSTR(place_of_supply, 1, 2),'') != ifnull(SUBSTR(company_gstin, 1, 2),'')
|
||||||
AND ABS(grand_total) > {0} AND (is_return = 1 OR is_debit_note = 1)
|
AND (is_return = 1 OR is_debit_note = 1)
|
||||||
AND IFNULL(gst_category, '') in ('Unregistered', 'Overseas')""".format(flt(b2c_limit))
|
AND IFNULL(gst_category, '') in ('Unregistered', 'Overseas')"""
|
||||||
|
|
||||||
elif self.filters.get("type_of_business") == "EXPORT":
|
elif self.filters.get("type_of_business") == "EXPORT":
|
||||||
conditions += """ AND is_return !=1 and gst_category = 'Overseas' """
|
conditions += """ AND is_return !=1 and gst_category = 'Overseas' """
|
||||||
|
|||||||
@@ -510,8 +510,14 @@
|
|||||||
"idx": 363,
|
"idx": 363,
|
||||||
"image_field": "image",
|
"image_field": "image",
|
||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"links": [],
|
"links": [
|
||||||
"modified": "2021-08-25 18:56:09.929905",
|
{
|
||||||
|
"group": "Allowed Items",
|
||||||
|
"link_doctype": "Party Specific Item",
|
||||||
|
"link_fieldname": "party"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"modified": "2021-09-06 17:38:54.196663",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Selling",
|
"module": "Selling",
|
||||||
"name": "Customer",
|
"name": "Customer",
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
// Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
|
// Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
|
||||||
// For license information, please see license.txt
|
// For license information, please see license.txt
|
||||||
|
|
||||||
frappe.ui.form.on('Supplier Item Group', {
|
frappe.ui.form.on('Party Specific Item', {
|
||||||
// refresh: function(frm) {
|
// refresh: function(frm) {
|
||||||
|
|
||||||
// }
|
// }
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
{
|
||||||
|
"actions": [],
|
||||||
|
"creation": "2021-08-27 19:28:07.559978",
|
||||||
|
"doctype": "DocType",
|
||||||
|
"editable_grid": 1,
|
||||||
|
"engine": "InnoDB",
|
||||||
|
"field_order": [
|
||||||
|
"party_type",
|
||||||
|
"party",
|
||||||
|
"column_break_3",
|
||||||
|
"restrict_based_on",
|
||||||
|
"based_on_value"
|
||||||
|
],
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldname": "party_type",
|
||||||
|
"fieldtype": "Select",
|
||||||
|
"in_list_view": 1,
|
||||||
|
"label": "Party Type",
|
||||||
|
"options": "Customer\nSupplier",
|
||||||
|
"reqd": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "party",
|
||||||
|
"fieldtype": "Dynamic Link",
|
||||||
|
"in_list_view": 1,
|
||||||
|
"label": "Party Name",
|
||||||
|
"options": "party_type",
|
||||||
|
"reqd": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "restrict_based_on",
|
||||||
|
"fieldtype": "Select",
|
||||||
|
"in_list_view": 1,
|
||||||
|
"label": "Restrict Items Based On",
|
||||||
|
"options": "Item\nItem Group\nBrand",
|
||||||
|
"reqd": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "column_break_3",
|
||||||
|
"fieldtype": "Column Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "based_on_value",
|
||||||
|
"fieldtype": "Dynamic Link",
|
||||||
|
"in_list_view": 1,
|
||||||
|
"label": "Based On Value",
|
||||||
|
"options": "restrict_based_on",
|
||||||
|
"reqd": 1
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"index_web_pages_for_search": 1,
|
||||||
|
"links": [],
|
||||||
|
"modified": "2021-09-14 13:27:58.612334",
|
||||||
|
"modified_by": "Administrator",
|
||||||
|
"module": "Selling",
|
||||||
|
"name": "Party Specific Item",
|
||||||
|
"owner": "Administrator",
|
||||||
|
"permissions": [
|
||||||
|
{
|
||||||
|
"create": 1,
|
||||||
|
"delete": 1,
|
||||||
|
"email": 1,
|
||||||
|
"export": 1,
|
||||||
|
"print": 1,
|
||||||
|
"read": 1,
|
||||||
|
"report": 1,
|
||||||
|
"role": "System Manager",
|
||||||
|
"share": 1,
|
||||||
|
"write": 1
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"sort_field": "modified",
|
||||||
|
"sort_order": "DESC",
|
||||||
|
"title_field": "party",
|
||||||
|
"track_changes": 1
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
|
||||||
|
# For license information, please see license.txt
|
||||||
|
|
||||||
|
import frappe
|
||||||
|
from frappe import _
|
||||||
|
from frappe.model.document import Document
|
||||||
|
|
||||||
|
|
||||||
|
class PartySpecificItem(Document):
|
||||||
|
def validate(self):
|
||||||
|
exists = frappe.db.exists({
|
||||||
|
'doctype': 'Party Specific Item',
|
||||||
|
'party_type': self.party_type,
|
||||||
|
'party': self.party,
|
||||||
|
'restrict_based_on': self.restrict_based_on,
|
||||||
|
'based_on': self.based_on_value,
|
||||||
|
})
|
||||||
|
if exists:
|
||||||
|
frappe.throw(_("This item filter has already been applied for the {0}").format(self.party_type))
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
|
||||||
|
# See license.txt
|
||||||
|
|
||||||
|
import unittest
|
||||||
|
|
||||||
|
import frappe
|
||||||
|
|
||||||
|
from erpnext.controllers.queries import item_query
|
||||||
|
|
||||||
|
test_dependencies = ['Item', 'Customer', 'Supplier']
|
||||||
|
|
||||||
|
def create_party_specific_item(**args):
|
||||||
|
psi = frappe.new_doc("Party Specific Item")
|
||||||
|
psi.party_type = args.get('party_type')
|
||||||
|
psi.party = args.get('party')
|
||||||
|
psi.restrict_based_on = args.get('restrict_based_on')
|
||||||
|
psi.based_on_value = args.get('based_on_value')
|
||||||
|
psi.insert()
|
||||||
|
|
||||||
|
class TestPartySpecificItem(unittest.TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
self.customer = frappe.get_last_doc("Customer")
|
||||||
|
self.supplier = frappe.get_last_doc("Supplier")
|
||||||
|
self.item = frappe.get_last_doc("Item")
|
||||||
|
|
||||||
|
def test_item_query_for_customer(self):
|
||||||
|
create_party_specific_item(party_type='Customer', party=self.customer.name, restrict_based_on='Item', based_on_value=self.item.name)
|
||||||
|
filters = {'is_sales_item': 1, 'customer': self.customer.name}
|
||||||
|
items = item_query(doctype= 'Item', txt= '', searchfield= 'name', start= 0, page_len= 20,filters=filters, as_dict= False)
|
||||||
|
for item in items:
|
||||||
|
self.assertEqual(item[0], self.item.name)
|
||||||
|
|
||||||
|
def test_item_query_for_supplier(self):
|
||||||
|
create_party_specific_item(party_type='Supplier', party=self.supplier.name, restrict_based_on='Item Group', based_on_value=self.item.item_group)
|
||||||
|
filters = {'supplier': self.supplier.name, 'is_purchase_item': 1}
|
||||||
|
items = item_query(doctype= 'Item', txt= '', searchfield= 'name', start= 0, page_len= 20,filters=filters, as_dict= False)
|
||||||
|
for item in items:
|
||||||
|
self.assertEqual(item[2], self.item.item_group)
|
||||||
@@ -25,7 +25,7 @@
|
|||||||
"editable_price_list_rate",
|
"editable_price_list_rate",
|
||||||
"validate_selling_price",
|
"validate_selling_price",
|
||||||
"editable_bundle_item_rates",
|
"editable_bundle_item_rates",
|
||||||
"transaction_settings_section",
|
"sales_transactions_settings_section",
|
||||||
"so_required",
|
"so_required",
|
||||||
"dn_required",
|
"dn_required",
|
||||||
"sales_update_frequency",
|
"sales_update_frequency",
|
||||||
@@ -143,15 +143,14 @@
|
|||||||
{
|
{
|
||||||
"default": "Stop",
|
"default": "Stop",
|
||||||
"depends_on": "maintain_same_sales_rate",
|
"depends_on": "maintain_same_sales_rate",
|
||||||
"description": "Configure the action to stop the transaction or just warn if the same rate is not maintained.",
|
|
||||||
"fieldname": "maintain_same_rate_action",
|
"fieldname": "maintain_same_rate_action",
|
||||||
"fieldtype": "Select",
|
"fieldtype": "Select",
|
||||||
"label": "Action If Same Rate is Not Maintained",
|
"label": "Action if Same Rate is Not Maintained Throughout Sales Cycle",
|
||||||
"mandatory_depends_on": "maintain_same_sales_rate",
|
"mandatory_depends_on": "maintain_same_sales_rate",
|
||||||
"options": "Stop\nWarn"
|
"options": "Stop\nWarn"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"depends_on": "eval: doc.maintain_same_rate_action == 'Stop'",
|
"depends_on": "eval: doc.maintain_same_sales_rate && doc.maintain_same_rate_action == 'Stop'",
|
||||||
"fieldname": "role_to_override_stop_action",
|
"fieldname": "role_to_override_stop_action",
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"label": "Role Allowed to Override Stop Action",
|
"label": "Role Allowed to Override Stop Action",
|
||||||
@@ -191,13 +190,15 @@
|
|||||||
"label": "Item Price Settings"
|
"label": "Item Price Settings"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "transaction_settings_section",
|
"fieldname": "sales_transactions_settings_section",
|
||||||
"fieldtype": "Section Break",
|
"fieldtype": "Section Break",
|
||||||
"label": "Transaction Settings"
|
"label": "Transaction Settings"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "column_break_5",
|
"default": "0",
|
||||||
"fieldtype": "Column Break"
|
"fieldname": "editable_bundle_item_rates",
|
||||||
|
"fieldtype": "Check",
|
||||||
|
"label": "Calculate Product Bundle Price based on Child Items' Rates"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"icon": "fa fa-cog",
|
"icon": "fa fa-cog",
|
||||||
@@ -205,7 +206,7 @@
|
|||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"issingle": 1,
|
"issingle": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2021-09-08 19:38:10.175989",
|
"modified": "2021-09-14 22:05:06.139820",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Selling",
|
"module": "Selling",
|
||||||
"name": "Selling Settings",
|
"name": "Selling Settings",
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ erpnext.selling.SellingController = erpnext.TransactionController.extend({
|
|||||||
this.frm.set_query("item_code", "items", function() {
|
this.frm.set_query("item_code", "items", function() {
|
||||||
return {
|
return {
|
||||||
query: "erpnext.controllers.queries.item_query",
|
query: "erpnext.controllers.queries.item_query",
|
||||||
filters: {'is_sales_item': 1}
|
filters: {'is_sales_item': 1, 'customer': cur_frm.doc.customer}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ class ItemGroup(NestedSet, WebsiteGenerator):
|
|||||||
self.parent_item_group = _('All Item Groups')
|
self.parent_item_group = _('All Item Groups')
|
||||||
|
|
||||||
self.make_route()
|
self.make_route()
|
||||||
|
self.validate_item_group_defaults()
|
||||||
|
|
||||||
def on_update(self):
|
def on_update(self):
|
||||||
NestedSet.on_update(self)
|
NestedSet.on_update(self)
|
||||||
@@ -113,6 +114,10 @@ class ItemGroup(NestedSet, WebsiteGenerator):
|
|||||||
def delete_child_item_groups_key(self):
|
def delete_child_item_groups_key(self):
|
||||||
frappe.cache().hdel("child_item_groups", self.name)
|
frappe.cache().hdel("child_item_groups", self.name)
|
||||||
|
|
||||||
|
def validate_item_group_defaults(self):
|
||||||
|
from erpnext.stock.doctype.item.item import validate_item_default_company_links
|
||||||
|
validate_item_default_company_links(self.item_group_defaults)
|
||||||
|
|
||||||
def get_child_groups_for_website(item_group_name, immediate=False):
|
def get_child_groups_for_website(item_group_name, immediate=False):
|
||||||
"""Returns child item groups *excluding* passed group."""
|
"""Returns child item groups *excluding* passed group."""
|
||||||
item_group = frappe.get_cached_value("Item Group", item_group_name, ["lft", "rgt"], as_dict=1)
|
item_group = frappe.get_cached_value("Item Group", item_group_name, ["lft", "rgt"], as_dict=1)
|
||||||
|
|||||||
@@ -7,7 +7,8 @@
|
|||||||
"item_group_defaults": [{
|
"item_group_defaults": [{
|
||||||
"company": "_Test Company",
|
"company": "_Test Company",
|
||||||
"buying_cost_center": "_Test Cost Center 2 - _TC",
|
"buying_cost_center": "_Test Cost Center 2 - _TC",
|
||||||
"selling_cost_center": "_Test Cost Center 2 - _TC"
|
"selling_cost_center": "_Test Cost Center 2 - _TC",
|
||||||
|
"default_warehouse": "_Test Warehouse - _TC"
|
||||||
}]
|
}]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -2116,9 +2116,9 @@
|
|||||||
},
|
},
|
||||||
|
|
||||||
"Saudi Arabia": {
|
"Saudi Arabia": {
|
||||||
"KSA VAT 5%": {
|
"KSA VAT 15%": {
|
||||||
"account_name": "VAT 5%",
|
"account_name": "VAT 15%",
|
||||||
"tax_rate": 5.00
|
"tax_rate": 15.00
|
||||||
},
|
},
|
||||||
"KSA VAT Zero": {
|
"KSA VAT Zero": {
|
||||||
"account_name": "VAT Zero",
|
"account_name": "VAT Zero",
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
|
|
||||||
import copy
|
import copy
|
||||||
import json
|
import json
|
||||||
|
from typing import List
|
||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
from frappe import _
|
from frappe import _
|
||||||
@@ -29,6 +30,7 @@ from erpnext.controllers.item_variant import (
|
|||||||
validate_item_variant_attributes,
|
validate_item_variant_attributes,
|
||||||
)
|
)
|
||||||
from erpnext.setup.doctype.item_group.item_group import invalidate_cache_for
|
from erpnext.setup.doctype.item_group.item_group import invalidate_cache_for
|
||||||
|
from erpnext.stock.doctype.item_default.item_default import ItemDefault
|
||||||
|
|
||||||
|
|
||||||
class DuplicateReorderRows(frappe.ValidationError):
|
class DuplicateReorderRows(frappe.ValidationError):
|
||||||
@@ -116,9 +118,9 @@ class Item(Document):
|
|||||||
self.validate_fixed_asset()
|
self.validate_fixed_asset()
|
||||||
self.validate_retain_sample()
|
self.validate_retain_sample()
|
||||||
self.validate_uom_conversion_factor()
|
self.validate_uom_conversion_factor()
|
||||||
self.validate_item_defaults()
|
|
||||||
self.validate_customer_provided_part()
|
self.validate_customer_provided_part()
|
||||||
self.update_defaults_from_item_group()
|
self.update_defaults_from_item_group()
|
||||||
|
self.validate_item_defaults()
|
||||||
self.validate_auto_reorder_enabled_in_stock_settings()
|
self.validate_auto_reorder_enabled_in_stock_settings()
|
||||||
self.cant_change()
|
self.cant_change()
|
||||||
self.validate_item_tax_net_rate_range()
|
self.validate_item_tax_net_rate_range()
|
||||||
@@ -309,8 +311,12 @@ class Item(Document):
|
|||||||
_("Default BOM ({0}) must be active for this item or its template").format(bom_item))
|
_("Default BOM ({0}) must be active for this item or its template").format(bom_item))
|
||||||
|
|
||||||
def fill_customer_code(self):
|
def fill_customer_code(self):
|
||||||
""" Append all the customer codes and insert into "customer_code" field of item table """
|
"""
|
||||||
self.customer_code = ','.join(d.ref_code for d in self.get("customer_items", []))
|
Append all the customer codes and insert into "customer_code" field of item table.
|
||||||
|
Used to search Item by customer code.
|
||||||
|
"""
|
||||||
|
customer_codes = set(d.ref_code for d in self.get("customer_items", []))
|
||||||
|
self.customer_code = ','.join(customer_codes)
|
||||||
|
|
||||||
def check_item_tax(self):
|
def check_item_tax(self):
|
||||||
"""Check whether Tax Rate is not entered twice for same Tax Type"""
|
"""Check whether Tax Rate is not entered twice for same Tax Type"""
|
||||||
@@ -526,9 +532,14 @@ class Item(Document):
|
|||||||
if len(companies) != len(self.item_defaults):
|
if len(companies) != len(self.item_defaults):
|
||||||
frappe.throw(_("Cannot set multiple Item Defaults for a company."))
|
frappe.throw(_("Cannot set multiple Item Defaults for a company."))
|
||||||
|
|
||||||
|
validate_item_default_company_links(self.item_defaults)
|
||||||
|
|
||||||
|
|
||||||
def update_defaults_from_item_group(self):
|
def update_defaults_from_item_group(self):
|
||||||
"""Get defaults from Item Group"""
|
"""Get defaults from Item Group"""
|
||||||
if self.item_group and not self.item_defaults:
|
if self.item_defaults or not self.item_group:
|
||||||
|
return
|
||||||
|
|
||||||
item_defaults = frappe.db.get_values("Item Default", {"parent": self.item_group},
|
item_defaults = frappe.db.get_values("Item Default", {"parent": self.item_group},
|
||||||
['company', 'default_warehouse','default_price_list','buying_cost_center','default_supplier',
|
['company', 'default_warehouse','default_price_list','buying_cost_center','default_supplier',
|
||||||
'expense_account','selling_cost_center','income_account'], as_dict = 1)
|
'expense_account','selling_cost_center','income_account'], as_dict = 1)
|
||||||
@@ -545,7 +556,6 @@ class Item(Document):
|
|||||||
'income_account': item.income_account
|
'income_account': item.income_account
|
||||||
})
|
})
|
||||||
else:
|
else:
|
||||||
warehouse = ''
|
|
||||||
defaults = frappe.defaults.get_defaults() or {}
|
defaults = frappe.defaults.get_defaults() or {}
|
||||||
|
|
||||||
# To check default warehouse is belong to the default company
|
# To check default warehouse is belong to the default company
|
||||||
@@ -1024,3 +1034,25 @@ def update_variants(variants, template, publish_progress=True):
|
|||||||
@erpnext.allow_regional
|
@erpnext.allow_regional
|
||||||
def set_item_tax_from_hsn_code(item):
|
def set_item_tax_from_hsn_code(item):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def validate_item_default_company_links(item_defaults: List[ItemDefault]) -> None:
|
||||||
|
for item_default in item_defaults:
|
||||||
|
for doctype, field in [
|
||||||
|
['Warehouse', 'default_warehouse'],
|
||||||
|
['Cost Center', 'buying_cost_center'],
|
||||||
|
['Cost Center', 'selling_cost_center'],
|
||||||
|
['Account', 'expense_account'],
|
||||||
|
['Account', 'income_account']
|
||||||
|
]:
|
||||||
|
if item_default.get(field):
|
||||||
|
company = frappe.db.get_value(doctype, item_default.get(field), 'company', cache=True)
|
||||||
|
if company and company != item_default.company:
|
||||||
|
frappe.throw(_("Row #{}: {} {} doesn't belong to Company {}. Please select valid {}.")
|
||||||
|
.format(
|
||||||
|
item_default.idx,
|
||||||
|
doctype,
|
||||||
|
frappe.bold(item_default.get(field)),
|
||||||
|
frappe.bold(item_default.company),
|
||||||
|
frappe.bold(frappe.unscrub(field))
|
||||||
|
), title=_("Invalid Item Defaults"))
|
||||||
|
|||||||
@@ -232,6 +232,23 @@ class TestItem(unittest.TestCase):
|
|||||||
for key, value in purchase_item_check.items():
|
for key, value in purchase_item_check.items():
|
||||||
self.assertEqual(value, purchase_item_details.get(key))
|
self.assertEqual(value, purchase_item_details.get(key))
|
||||||
|
|
||||||
|
def test_item_default_validations(self):
|
||||||
|
|
||||||
|
with self.assertRaises(frappe.ValidationError) as ve:
|
||||||
|
make_item("Bad Item defaults", {
|
||||||
|
"item_group": "_Test Item Group",
|
||||||
|
"item_defaults": [{
|
||||||
|
"company": "_Test Company 1",
|
||||||
|
"default_warehouse": "_Test Warehouse - _TC",
|
||||||
|
"expense_account": "Stock In Hand - _TC",
|
||||||
|
"buying_cost_center": "_Test Cost Center - _TC",
|
||||||
|
"selling_cost_center": "_Test Cost Center - _TC",
|
||||||
|
}]
|
||||||
|
})
|
||||||
|
|
||||||
|
self.assertTrue("belong to company" in str(ve.exception).lower(),
|
||||||
|
msg="Mismatching company entities in item defaults should not be allowed.")
|
||||||
|
|
||||||
def test_item_attribute_change_after_variant(self):
|
def test_item_attribute_change_after_variant(self):
|
||||||
frappe.delete_doc_if_exists("Item", "_Test Variant Item-L", force=1)
|
frappe.delete_doc_if_exists("Item", "_Test Variant Item-L", force=1)
|
||||||
|
|
||||||
|
|||||||
@@ -272,8 +272,9 @@ def update_status(name, status):
|
|||||||
material_request.update_status(status)
|
material_request.update_status(status)
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def make_purchase_order(source_name, target_doc=None, args={}):
|
def make_purchase_order(source_name, target_doc=None, args=None):
|
||||||
|
if args is None:
|
||||||
|
args = {}
|
||||||
if isinstance(args, string_types):
|
if isinstance(args, string_types):
|
||||||
args = json.loads(args)
|
args = json.loads(args)
|
||||||
|
|
||||||
|
|||||||
@@ -44,8 +44,10 @@ def update_packing_list_item(doc, packing_item_code, qty, main_item_row, descrip
|
|||||||
# check if exists
|
# check if exists
|
||||||
exists = 0
|
exists = 0
|
||||||
for d in doc.get("packed_items"):
|
for d in doc.get("packed_items"):
|
||||||
if d.parent_item == main_item_row.item_code and d.item_code == packing_item_code and\
|
if d.parent_item == main_item_row.item_code and d.item_code == packing_item_code:
|
||||||
d.parent_detail_docname == main_item_row.name:
|
if d.parent_detail_docname != main_item_row.name:
|
||||||
|
d.parent_detail_docname = main_item_row.name
|
||||||
|
|
||||||
pi, exists = d, 1
|
pi, exists = d, 1
|
||||||
break
|
break
|
||||||
|
|
||||||
|
|||||||
@@ -36,7 +36,8 @@
|
|||||||
"fieldname": "qty",
|
"fieldname": "qty",
|
||||||
"fieldtype": "Float",
|
"fieldtype": "Float",
|
||||||
"in_list_view": 1,
|
"in_list_view": 1,
|
||||||
"label": "Qty"
|
"label": "Qty",
|
||||||
|
"reqd": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "picked_qty",
|
"fieldname": "picked_qty",
|
||||||
@@ -180,7 +181,7 @@
|
|||||||
],
|
],
|
||||||
"istable": 1,
|
"istable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2020-06-24 17:18:57.357120",
|
"modified": "2021-09-28 12:02:16.923056",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Stock",
|
"module": "Stock",
|
||||||
"name": "Pick List Item",
|
"name": "Pick List Item",
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
import json
|
import json
|
||||||
|
from collections import defaultdict
|
||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
from frappe import _
|
from frappe import _
|
||||||
@@ -684,7 +685,7 @@ class StockEntry(StockController):
|
|||||||
|
|
||||||
def validate_bom(self):
|
def validate_bom(self):
|
||||||
for d in self.get('items'):
|
for d in self.get('items'):
|
||||||
if d.bom_no and (d.t_warehouse != getattr(self, "pro_doc", frappe._dict()).scrap_warehouse):
|
if d.bom_no and d.is_finished_item:
|
||||||
item_code = d.original_item or d.item_code
|
item_code = d.original_item or d.item_code
|
||||||
validate_bom_no(item_code, d.bom_no)
|
validate_bom_no(item_code, d.bom_no)
|
||||||
|
|
||||||
@@ -1191,13 +1192,88 @@ class StockEntry(StockController):
|
|||||||
|
|
||||||
# item dict = { item_code: {qty, description, stock_uom} }
|
# item dict = { item_code: {qty, description, stock_uom} }
|
||||||
item_dict = get_bom_items_as_dict(self.bom_no, self.company, qty=qty,
|
item_dict = get_bom_items_as_dict(self.bom_no, self.company, qty=qty,
|
||||||
fetch_exploded = 0, fetch_scrap_items = 1)
|
fetch_exploded = 0, fetch_scrap_items = 1) or {}
|
||||||
|
|
||||||
for item in itervalues(item_dict):
|
for item in itervalues(item_dict):
|
||||||
item.from_warehouse = ""
|
item.from_warehouse = ""
|
||||||
item.is_scrap_item = 1
|
item.is_scrap_item = 1
|
||||||
|
|
||||||
|
for row in self.get_scrap_items_from_job_card():
|
||||||
|
if row.stock_qty <= 0:
|
||||||
|
continue
|
||||||
|
|
||||||
|
item_row = item_dict.get(row.item_code)
|
||||||
|
if not item_row:
|
||||||
|
item_row = frappe._dict({})
|
||||||
|
|
||||||
|
item_row.update({
|
||||||
|
'uom': row.stock_uom,
|
||||||
|
'from_warehouse': '',
|
||||||
|
'qty': row.stock_qty + flt(item_row.stock_qty),
|
||||||
|
'converison_factor': 1,
|
||||||
|
'is_scrap_item': 1,
|
||||||
|
'item_name': row.item_name,
|
||||||
|
'description': row.description,
|
||||||
|
'allow_zero_valuation_rate': 1
|
||||||
|
})
|
||||||
|
|
||||||
|
item_dict[row.item_code] = item_row
|
||||||
|
|
||||||
return item_dict
|
return item_dict
|
||||||
|
|
||||||
|
def get_scrap_items_from_job_card(self):
|
||||||
|
if not self.pro_doc:
|
||||||
|
self.set_work_order_details()
|
||||||
|
|
||||||
|
scrap_items = frappe.db.sql('''
|
||||||
|
SELECT
|
||||||
|
JCSI.item_code, JCSI.item_name, SUM(JCSI.stock_qty) as stock_qty, JCSI.stock_uom, JCSI.description
|
||||||
|
FROM
|
||||||
|
`tabJob Card` JC, `tabJob Card Scrap Item` JCSI
|
||||||
|
WHERE
|
||||||
|
JCSI.parent = JC.name AND JC.docstatus = 1
|
||||||
|
AND JCSI.item_code IS NOT NULL AND JC.work_order = %s
|
||||||
|
GROUP BY
|
||||||
|
JCSI.item_code
|
||||||
|
''', self.work_order, as_dict=1)
|
||||||
|
|
||||||
|
pending_qty = flt(self.pro_doc.qty) - flt(self.pro_doc.produced_qty)
|
||||||
|
if pending_qty <=0:
|
||||||
|
return []
|
||||||
|
|
||||||
|
used_scrap_items = self.get_used_scrap_items()
|
||||||
|
for row in scrap_items:
|
||||||
|
row.stock_qty -= flt(used_scrap_items.get(row.item_code))
|
||||||
|
row.stock_qty = (row.stock_qty) * flt(self.fg_completed_qty) / flt(pending_qty)
|
||||||
|
|
||||||
|
if used_scrap_items.get(row.item_code):
|
||||||
|
used_scrap_items[row.item_code] -= row.stock_qty
|
||||||
|
|
||||||
|
if cint(frappe.get_cached_value('UOM', row.stock_uom, 'must_be_whole_number')):
|
||||||
|
row.stock_qty = frappe.utils.ceil(row.stock_qty)
|
||||||
|
|
||||||
|
return scrap_items
|
||||||
|
|
||||||
|
def get_used_scrap_items(self):
|
||||||
|
used_scrap_items = defaultdict(float)
|
||||||
|
data = frappe.get_all(
|
||||||
|
'Stock Entry',
|
||||||
|
fields = [
|
||||||
|
'`tabStock Entry Detail`.`item_code`', '`tabStock Entry Detail`.`qty`'
|
||||||
|
],
|
||||||
|
filters = [
|
||||||
|
['Stock Entry', 'work_order', '=', self.work_order],
|
||||||
|
['Stock Entry Detail', 'is_scrap_item', '=', 1],
|
||||||
|
['Stock Entry', 'docstatus', '=', 1],
|
||||||
|
['Stock Entry', 'purpose', 'in', ['Repack', 'Manufacture']]
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
for row in data:
|
||||||
|
used_scrap_items[row.item_code] += row.qty
|
||||||
|
|
||||||
|
return used_scrap_items
|
||||||
|
|
||||||
def get_unconsumed_raw_materials(self):
|
def get_unconsumed_raw_materials(self):
|
||||||
wo = frappe.get_doc("Work Order", self.work_order)
|
wo = frappe.get_doc("Work Order", self.work_order)
|
||||||
wo_items = frappe.get_all('Work Order Item',
|
wo_items = frappe.get_all('Work Order Item',
|
||||||
@@ -1417,8 +1493,8 @@ class StockEntry(StockController):
|
|||||||
se_child.is_scrap_item = item_dict[d].get("is_scrap_item", 0)
|
se_child.is_scrap_item = item_dict[d].get("is_scrap_item", 0)
|
||||||
se_child.is_process_loss = item_dict[d].get("is_process_loss", 0)
|
se_child.is_process_loss = item_dict[d].get("is_process_loss", 0)
|
||||||
|
|
||||||
for field in ["idx", "po_detail", "original_item",
|
for field in ["idx", "po_detail", "original_item", "expense_account",
|
||||||
"expense_account", "description", "item_name", "serial_no", "batch_no"]:
|
"description", "item_name", "serial_no", "batch_no", "allow_zero_valuation_rate"]:
|
||||||
if item_dict[d].get(field):
|
if item_dict[d].get(field):
|
||||||
se_child.set(field, item_dict[d].get(field))
|
se_child.set(field, item_dict[d].get(field))
|
||||||
|
|
||||||
|
|||||||
@@ -592,6 +592,11 @@ def get_stock_balance_for(item_code, warehouse,
|
|||||||
item_dict = frappe.db.get_value("Item", item_code,
|
item_dict = frappe.db.get_value("Item", item_code,
|
||||||
["has_serial_no", "has_batch_no"], as_dict=1)
|
["has_serial_no", "has_batch_no"], as_dict=1)
|
||||||
|
|
||||||
|
if not item_dict:
|
||||||
|
# In cases of data upload to Items table
|
||||||
|
msg = _("Item {} does not exist.").format(item_code)
|
||||||
|
frappe.throw(msg, title=_("Missing"))
|
||||||
|
|
||||||
serial_nos = ""
|
serial_nos = ""
|
||||||
with_serial_no = True if item_dict.get("has_serial_no") else False
|
with_serial_no = True if item_dict.get("has_serial_no") else False
|
||||||
data = get_stock_balance(item_code, warehouse, posting_date, posting_time,
|
data = get_stock_balance(item_code, warehouse, posting_date, posting_time,
|
||||||
|
|||||||
63
erpnext/stock/report/test_reports.py
Normal file
63
erpnext/stock/report/test_reports.py
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
import unittest
|
||||||
|
from typing import List, Tuple
|
||||||
|
|
||||||
|
from erpnext.tests.utils import ReportFilters, ReportName, execute_script_report
|
||||||
|
|
||||||
|
DEFAULT_FILTERS = {
|
||||||
|
"company": "_Test Company",
|
||||||
|
"from_date": "2010-01-01",
|
||||||
|
"to_date": "2030-01-01",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
REPORT_FILTER_TEST_CASES: List[Tuple[ReportName, ReportFilters]] = [
|
||||||
|
("Stock Ledger", {"_optional": True}),
|
||||||
|
("Stock Balance", {"_optional": True}),
|
||||||
|
("Stock Projected Qty", {"_optional": True}),
|
||||||
|
("Batch-Wise Balance History", {}),
|
||||||
|
("Itemwise Recommended Reorder Level", {"item_group": "All Item Groups"}),
|
||||||
|
("COGS By Item Group", {}),
|
||||||
|
("Stock Qty vs Serial No Count", {"warehouse": "_Test Warehouse - _TC"}),
|
||||||
|
(
|
||||||
|
"Stock and Account Value Comparison",
|
||||||
|
{
|
||||||
|
"company": "_Test Company with perpetual inventory",
|
||||||
|
"account": "Stock In Hand - TCP1",
|
||||||
|
"as_on_date": "2021-01-01",
|
||||||
|
},
|
||||||
|
),
|
||||||
|
("Product Bundle Balance", {"date": "2022-01-01", "_optional": True}),
|
||||||
|
(
|
||||||
|
"Stock Analytics",
|
||||||
|
{
|
||||||
|
"from_date": "2021-01-01",
|
||||||
|
"to_date": "2021-12-31",
|
||||||
|
"value_quantity": "Quantity",
|
||||||
|
"_optional": True,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
("Warehouse wise Item Balance Age and Value", {"_optional": True}),
|
||||||
|
("Item Variant Details", {"item": "_Test Variant Item",}),
|
||||||
|
("Total Stock Summary", {"group_by": "warehouse",}),
|
||||||
|
("Batch Item Expiry Status", {}),
|
||||||
|
("Stock Ageing", {"range1": 30, "range2": 60, "range3": 90, "_optional": True}),
|
||||||
|
]
|
||||||
|
|
||||||
|
OPTIONAL_FILTERS = {
|
||||||
|
"warehouse": "_Test Warehouse - _TC",
|
||||||
|
"item": "_Test Item",
|
||||||
|
"item_group": "_Test Item Group",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class TestReports(unittest.TestCase):
|
||||||
|
def test_execute_all_stock_reports(self):
|
||||||
|
"""Test that all script report in stock modules are executable with supported filters"""
|
||||||
|
for report, filter in REPORT_FILTER_TEST_CASES:
|
||||||
|
execute_script_report(
|
||||||
|
report_name=report,
|
||||||
|
module="Stock",
|
||||||
|
filters=filter,
|
||||||
|
default_filters=DEFAULT_FILTERS,
|
||||||
|
optional_filters=OPTIONAL_FILTERS if filter.get("_optional") else None,
|
||||||
|
)
|
||||||
@@ -399,6 +399,7 @@ class update_entries_after(object):
|
|||||||
return
|
return
|
||||||
|
|
||||||
# Get dynamic incoming/outgoing rate
|
# Get dynamic incoming/outgoing rate
|
||||||
|
if not self.args.get("sle_id"):
|
||||||
self.get_dynamic_incoming_outgoing_rate(sle)
|
self.get_dynamic_incoming_outgoing_rate(sle)
|
||||||
|
|
||||||
if sle.serial_no:
|
if sle.serial_no:
|
||||||
@@ -439,6 +440,7 @@ class update_entries_after(object):
|
|||||||
sle.doctype="Stock Ledger Entry"
|
sle.doctype="Stock Ledger Entry"
|
||||||
frappe.get_doc(sle).db_update()
|
frappe.get_doc(sle).db_update()
|
||||||
|
|
||||||
|
if not self.args.get("sle_id"):
|
||||||
self.update_outgoing_rate_on_transaction(sle)
|
self.update_outgoing_rate_on_transaction(sle)
|
||||||
|
|
||||||
def validate_negative_stock(self, sle):
|
def validate_negative_stock(self, sle):
|
||||||
|
|||||||
@@ -11,6 +11,6 @@
|
|||||||
{% endfor %}
|
{% endfor %}
|
||||||
</ol>
|
</ol>
|
||||||
{% else %}
|
{% else %}
|
||||||
<p>You don't have no upcoming holidays this {{ frequency }}.</p>
|
<p>You have no upcoming holidays this {{ frequency }}.</p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ $.extend(shopping_cart, {
|
|||||||
callback: function(r) {
|
callback: function(r) {
|
||||||
d.hide();
|
d.hide();
|
||||||
if (!r.exc) {
|
if (!r.exc) {
|
||||||
$(".cart-tax-items").html(r.message.taxes);
|
$(".cart-tax-items").html(r.message.total);
|
||||||
shopping_cart.parent.find(
|
shopping_cart.parent.find(
|
||||||
`.address-container[data-address-type="${address_type}"]`
|
`.address-container[data-address-type="${address_type}"]`
|
||||||
).html(r.message.address);
|
).html(r.message.address);
|
||||||
@@ -214,12 +214,15 @@ $.extend(shopping_cart, {
|
|||||||
},
|
},
|
||||||
|
|
||||||
place_order: function(btn) {
|
place_order: function(btn) {
|
||||||
|
shopping_cart.freeze();
|
||||||
|
|
||||||
return frappe.call({
|
return frappe.call({
|
||||||
type: "POST",
|
type: "POST",
|
||||||
method: "erpnext.e_commerce.shopping_cart.cart.place_order",
|
method: "erpnext.e_commerce.shopping_cart.cart.place_order",
|
||||||
btn: btn,
|
btn: btn,
|
||||||
callback: function(r) {
|
callback: function(r) {
|
||||||
if(r.exc) {
|
if(r.exc) {
|
||||||
|
shopping_cart.unfreeze();
|
||||||
var msg = "";
|
var msg = "";
|
||||||
if(r._server_messages) {
|
if(r._server_messages) {
|
||||||
msg = JSON.parse(r._server_messages || []).join("<br>");
|
msg = JSON.parse(r._server_messages || []).join("<br>");
|
||||||
@@ -230,7 +233,6 @@ $.extend(shopping_cart, {
|
|||||||
.html(msg || frappe._("Something went wrong!"))
|
.html(msg || frappe._("Something went wrong!"))
|
||||||
.toggle(true);
|
.toggle(true);
|
||||||
} else {
|
} else {
|
||||||
$('.cart-container table').hide();
|
|
||||||
$(btn).hide();
|
$(btn).hide();
|
||||||
window.location.href = '/orders/' + encodeURIComponent(r.message);
|
window.location.href = '/orders/' + encodeURIComponent(r.message);
|
||||||
}
|
}
|
||||||
@@ -239,12 +241,15 @@ $.extend(shopping_cart, {
|
|||||||
},
|
},
|
||||||
|
|
||||||
request_quotation: function(btn) {
|
request_quotation: function(btn) {
|
||||||
|
shopping_cart.freeze();
|
||||||
|
|
||||||
return frappe.call({
|
return frappe.call({
|
||||||
type: "POST",
|
type: "POST",
|
||||||
method: "erpnext.e_commerce.shopping_cart.cart.request_for_quotation",
|
method: "erpnext.e_commerce.shopping_cart.cart.request_for_quotation",
|
||||||
btn: btn,
|
btn: btn,
|
||||||
callback: function(r) {
|
callback: function(r) {
|
||||||
if(r.exc) {
|
if(r.exc) {
|
||||||
|
shopping_cart.unfreeze();
|
||||||
var msg = "";
|
var msg = "";
|
||||||
if(r._server_messages) {
|
if(r._server_messages) {
|
||||||
msg = JSON.parse(r._server_messages || []).join("<br>");
|
msg = JSON.parse(r._server_messages || []).join("<br>");
|
||||||
@@ -255,7 +260,6 @@ $.extend(shopping_cart, {
|
|||||||
.html(msg || frappe._("Something went wrong!"))
|
.html(msg || frappe._("Something went wrong!"))
|
||||||
.toggle(true);
|
.toggle(true);
|
||||||
} else {
|
} else {
|
||||||
$('.cart-container table').hide();
|
|
||||||
$(btn).hide();
|
$(btn).hide();
|
||||||
window.location.href = '/quotations/' + encodeURIComponent(r.message);
|
window.location.href = '/quotations/' + encodeURIComponent(r.message);
|
||||||
}
|
}
|
||||||
|
|||||||
10
erpnext/templates/includes/cart/cart_items_total.html
Normal file
10
erpnext/templates/includes/cart/cart_items_total.html
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
<!-- Total at the end of the cart items -->
|
||||||
|
<tr>
|
||||||
|
<th></th>
|
||||||
|
<th class="text-left item-grand-total" colspan="1">
|
||||||
|
{{ _("Total") }}
|
||||||
|
</th>
|
||||||
|
<th class="text-left item-grand-total totals" colspan="3">
|
||||||
|
{{ doc.get_formatted("total") }}
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
@@ -1,13 +1,13 @@
|
|||||||
<!-- Payment -->
|
<!-- Payment -->
|
||||||
<div class="mb-3 frappe-card p-5 payment-summary">
|
<h6>
|
||||||
<h6>
|
|
||||||
{{ _("Payment Summary") }}
|
{{ _("Payment Summary") }}
|
||||||
</h6>
|
</h6>
|
||||||
<div class="card h-100">
|
<div class="card h-100">
|
||||||
<div class="card-body p-0">
|
<div class="card-body p-0">
|
||||||
<table class="table w-100">
|
<table class="table w-100">
|
||||||
<tr>
|
<tr>
|
||||||
<td class="bill-label">{{ _("Net Total (") + frappe.utils.cstr(doc.items|len) + _(" Items)") }}</td>
|
{% set total_items = frappe.utils.cstr(frappe.utils.flt(doc.total_qty, 0)) %}
|
||||||
|
<td class="bill-label">{{ _("Net Total (") + total_items + _(" Items)") }}</td>
|
||||||
<td class="bill-content net-total text-right">{{ doc.get_formatted("net_total") }}</td>
|
<td class="bill-content net-total text-right">{{ doc.get_formatted("net_total") }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
||||||
@@ -57,7 +57,6 @@
|
|||||||
</button>
|
</button>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- TODO: Apply Coupon Dialog-->
|
<!-- TODO: Apply Coupon Dialog-->
|
||||||
|
|||||||
@@ -45,15 +45,7 @@
|
|||||||
|
|
||||||
{% if cart_settings.enable_checkout or cart_settings.show_price_in_quotation %}
|
{% if cart_settings.enable_checkout or cart_settings.show_price_in_quotation %}
|
||||||
<tfoot class="cart-tax-items">
|
<tfoot class="cart-tax-items">
|
||||||
<tr>
|
{% include "templates/includes/cart/cart_items_total.html" %}
|
||||||
<th></th>
|
|
||||||
<th class="text-left item-grand-total" colspan="1">
|
|
||||||
{{ _("Total") }}
|
|
||||||
</th>
|
|
||||||
<th class="text-left item-grand-total totals" colspan="3">
|
|
||||||
{{ doc.get_formatted("total") }}
|
|
||||||
</th>
|
|
||||||
</tr>
|
|
||||||
</tfoot>
|
</tfoot>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</table>
|
</table>
|
||||||
@@ -110,7 +102,9 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% if cart_settings.enable_checkout %}
|
{% if cart_settings.enable_checkout %}
|
||||||
|
<div class="mb-3 frappe-card p-5 payment-summary">
|
||||||
{% include "templates/includes/cart/cart_payment_summary.html" %}
|
{% include "templates/includes/cart/cart_payment_summary.html" %}
|
||||||
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% include "templates/includes/cart/cart_address.html" %}
|
{% include "templates/includes/cart/cart_address.html" %}
|
||||||
@@ -126,11 +120,11 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="cart-empty-message mt-4">{{ _('Your cart is Empty') }}</p>
|
<div class="cart-empty-message mt-4">{{ _('Your cart is Empty') }}</p>
|
||||||
{% if cart_settings.enable_checkout %}
|
{% if cart_settings.enable_checkout %}
|
||||||
<a class="btn btn-outline-primary" href="/orders">
|
<a class="btn btn-outline-primary" href="/orders" style="font-size: 16px;">
|
||||||
{{ _('See past orders') }}
|
{{ _('See past orders') }}
|
||||||
</a>
|
</a>
|
||||||
{% else %}
|
{% else %}
|
||||||
<a class="btn btn-outline-primary" href="/quotations">
|
<a class="btn btn-outline-primary" href="/quotations" style="font-size: 16px;">
|
||||||
{{ _('See past quotations') }}
|
{{ _('See past quotations') }}
|
||||||
</a>
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
</div>
|
</div>
|
||||||
{% else %}
|
{% else %}
|
||||||
<div class="col-xs-5 {%- if doc.align_labels_right %} text-right{%- endif -%}">
|
<div class="col-xs-5 {%- if doc.align_labels_right %} text-right{%- endif -%}">
|
||||||
<label>{{ _(doc.meta.get_label('total')) }}</label></div>
|
<label>{{ _(df.label) }}</label></div>
|
||||||
<div class="col-xs-7 text-right">
|
<div class="col-xs-7 text-right">
|
||||||
{{ doc.get_formatted("total", doc) }}
|
{{ doc.get_formatted("total", doc) }}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
138
erpnext/tests/test_webform.py
Normal file
138
erpnext/tests/test_webform.py
Normal file
@@ -0,0 +1,138 @@
|
|||||||
|
import unittest
|
||||||
|
|
||||||
|
import frappe
|
||||||
|
|
||||||
|
from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order
|
||||||
|
|
||||||
|
|
||||||
|
class TestWebsite(unittest.TestCase):
|
||||||
|
def test_permission_for_custom_doctype(self):
|
||||||
|
create_user('Supplier 1', 'supplier1@gmail.com')
|
||||||
|
create_user('Supplier 2', 'supplier2@gmail.com')
|
||||||
|
create_supplier_with_contact('Supplier1', 'All Supplier Groups', 'Supplier 1', 'supplier1@gmail.com')
|
||||||
|
create_supplier_with_contact('Supplier2', 'All Supplier Groups', 'Supplier 2', 'supplier2@gmail.com')
|
||||||
|
po1 = create_purchase_order(supplier='Supplier1')
|
||||||
|
po2 = create_purchase_order(supplier='Supplier2')
|
||||||
|
|
||||||
|
create_custom_doctype()
|
||||||
|
create_webform()
|
||||||
|
create_order_assignment(supplier='Supplier1', po = po1.name)
|
||||||
|
create_order_assignment(supplier='Supplier2', po = po2.name)
|
||||||
|
|
||||||
|
frappe.set_user("Administrator")
|
||||||
|
# checking if data consist of all order assignment of Supplier1 and Supplier2
|
||||||
|
self.assertTrue('Supplier1' and 'Supplier2' in [data.supplier for data in get_data()])
|
||||||
|
|
||||||
|
frappe.set_user("supplier1@gmail.com")
|
||||||
|
# checking if data only consist of order assignment of Supplier1
|
||||||
|
self.assertTrue('Supplier1' in [data.supplier for data in get_data()])
|
||||||
|
self.assertFalse([data.supplier for data in get_data() if data.supplier != 'Supplier1'])
|
||||||
|
|
||||||
|
frappe.set_user("supplier2@gmail.com")
|
||||||
|
# checking if data only consist of order assignment of Supplier2
|
||||||
|
self.assertTrue('Supplier2' in [data.supplier for data in get_data()])
|
||||||
|
self.assertFalse([data.supplier for data in get_data() if data.supplier != 'Supplier2'])
|
||||||
|
|
||||||
|
frappe.set_user("Administrator")
|
||||||
|
|
||||||
|
def get_data():
|
||||||
|
webform_list_contexts = frappe.get_hooks('webform_list_context')
|
||||||
|
if webform_list_contexts:
|
||||||
|
context = frappe._dict(frappe.get_attr(webform_list_contexts[0])('Buying') or {})
|
||||||
|
kwargs = dict(doctype='Order Assignment', order_by = 'modified desc')
|
||||||
|
return context.get_list(**kwargs)
|
||||||
|
|
||||||
|
def create_user(name, email):
|
||||||
|
frappe.get_doc({
|
||||||
|
'doctype': 'User',
|
||||||
|
'send_welcome_email': 0,
|
||||||
|
'user_type': 'Website User',
|
||||||
|
'first_name': name,
|
||||||
|
'email': email,
|
||||||
|
'roles': [{"doctype": "Has Role", "role": "Supplier"}]
|
||||||
|
}).insert(ignore_if_duplicate = True)
|
||||||
|
|
||||||
|
def create_supplier_with_contact(name, group, contact_name, contact_email):
|
||||||
|
supplier = frappe.get_doc({
|
||||||
|
'doctype': 'Supplier',
|
||||||
|
'supplier_name': name,
|
||||||
|
'supplier_group': group
|
||||||
|
}).insert(ignore_if_duplicate = True)
|
||||||
|
|
||||||
|
if not frappe.db.exists('Contact', contact_name+'-1-'+name):
|
||||||
|
new_contact = frappe.new_doc("Contact")
|
||||||
|
new_contact.first_name = contact_name
|
||||||
|
new_contact.is_primary_contact = True,
|
||||||
|
new_contact.append('links', {
|
||||||
|
"link_doctype": "Supplier",
|
||||||
|
"link_name": supplier.name
|
||||||
|
})
|
||||||
|
new_contact.append('email_ids', {
|
||||||
|
"email_id": contact_email,
|
||||||
|
"is_primary": 1
|
||||||
|
})
|
||||||
|
|
||||||
|
new_contact.insert(ignore_mandatory=True)
|
||||||
|
|
||||||
|
def create_custom_doctype():
|
||||||
|
frappe.get_doc({
|
||||||
|
'doctype': 'DocType',
|
||||||
|
'name': 'Order Assignment',
|
||||||
|
'module': 'Buying',
|
||||||
|
'custom': 1,
|
||||||
|
'autoname': 'field:po',
|
||||||
|
'fields': [
|
||||||
|
{'label': 'PO', 'fieldname': 'po', 'fieldtype': 'Link', 'options': 'Purchase Order'},
|
||||||
|
{'label': 'Supplier', 'fieldname': 'supplier', 'fieldtype': 'Data', "fetch_from": "po.supplier"}
|
||||||
|
],
|
||||||
|
'permissions': [
|
||||||
|
{
|
||||||
|
"create": 1,
|
||||||
|
"delete": 1,
|
||||||
|
"email": 1,
|
||||||
|
"export": 1,
|
||||||
|
"print": 1,
|
||||||
|
"read": 1,
|
||||||
|
"report": 1,
|
||||||
|
"role": "System Manager",
|
||||||
|
"share": 1,
|
||||||
|
"write": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"read": 1,
|
||||||
|
"role": "Supplier"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}).insert(ignore_if_duplicate = True)
|
||||||
|
|
||||||
|
def create_webform():
|
||||||
|
frappe.get_doc({
|
||||||
|
'doctype': 'Web Form',
|
||||||
|
'module': 'Buying',
|
||||||
|
'title': 'SO Schedule',
|
||||||
|
'route': 'so-schedule',
|
||||||
|
'doc_type': 'Order Assignment',
|
||||||
|
'web_form_fields': [
|
||||||
|
{
|
||||||
|
'doctype': 'Web Form Field',
|
||||||
|
'fieldname': 'po',
|
||||||
|
'fieldtype': 'Link',
|
||||||
|
'options': 'Purchase Order',
|
||||||
|
'label': 'PO'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'doctype': 'Web Form Field',
|
||||||
|
'fieldname': 'supplier',
|
||||||
|
'fieldtype': 'Data',
|
||||||
|
'label': 'Supplier'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
}).insert(ignore_if_duplicate = True)
|
||||||
|
|
||||||
|
def create_order_assignment(supplier, po):
|
||||||
|
frappe.get_doc({
|
||||||
|
'doctype': 'Order Assignment',
|
||||||
|
'po': po,
|
||||||
|
'supplier': supplier,
|
||||||
|
}).insert(ignore_if_duplicate = True)
|
||||||
@@ -3,8 +3,13 @@
|
|||||||
|
|
||||||
import copy
|
import copy
|
||||||
from contextlib import contextmanager
|
from contextlib import contextmanager
|
||||||
|
from typing import Any, Dict, NewType, Optional
|
||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
|
from frappe.core.doctype.report.report import get_report_module_dotted_path
|
||||||
|
|
||||||
|
ReportFilters = Dict[str, Any]
|
||||||
|
ReportName = NewType("ReportName", str)
|
||||||
|
|
||||||
|
|
||||||
def create_test_contact_and_address():
|
def create_test_contact_and_address():
|
||||||
@@ -78,3 +83,39 @@ def change_settings(doctype, settings_dict):
|
|||||||
for key, value in previous_settings.items():
|
for key, value in previous_settings.items():
|
||||||
setattr(settings, key, value)
|
setattr(settings, key, value)
|
||||||
settings.save()
|
settings.save()
|
||||||
|
|
||||||
|
|
||||||
|
def execute_script_report(
|
||||||
|
report_name: ReportName,
|
||||||
|
module: str,
|
||||||
|
filters: ReportFilters,
|
||||||
|
default_filters: Optional[ReportFilters] = None,
|
||||||
|
optional_filters: Optional[ReportFilters] = None
|
||||||
|
):
|
||||||
|
"""Util for testing execution of a report with specified filters.
|
||||||
|
|
||||||
|
Tests the execution of report with default_filters + filters.
|
||||||
|
Tests the execution using optional_filters one at a time.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
report_name: Human readable name of report (unscrubbed)
|
||||||
|
module: module to which report belongs to
|
||||||
|
filters: specific values for filters
|
||||||
|
default_filters: default values for filters such as company name.
|
||||||
|
optional_filters: filters which should be tested one at a time in addition to default filters.
|
||||||
|
"""
|
||||||
|
|
||||||
|
if default_filters is None:
|
||||||
|
default_filters = {}
|
||||||
|
|
||||||
|
report_execute_fn = frappe.get_attr(get_report_module_dotted_path(module, report_name) + ".execute")
|
||||||
|
report_filters = frappe._dict(default_filters).copy().update(filters)
|
||||||
|
|
||||||
|
report_data = report_execute_fn(report_filters)
|
||||||
|
|
||||||
|
if optional_filters:
|
||||||
|
for key, value in optional_filters.items():
|
||||||
|
filter_with_optional_param = report_filters.copy().update({key: value})
|
||||||
|
report_execute_fn(filter_with_optional_param)
|
||||||
|
|
||||||
|
return report_data
|
||||||
|
|||||||
Reference in New Issue
Block a user