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

This commit is contained in:
Rohit Waghchaure
2021-10-30 15:04:29 +05:30
18 changed files with 265 additions and 36 deletions

View File

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

View File

@@ -36,5 +36,20 @@ frappe.query_reports["Gross Profit"] = {
"options": "Invoice\nItem Code\nItem Group\nBrand\nWarehouse\nCustomer\nCustomer Group\nTerritory\nSales Person\nProject",
"default": "Invoice"
},
]
],
"tree": true,
"name_field": "parent",
"parent_field": "parent_invoice",
"initial_depth": 3,
"formatter": function(value, row, column, data, default_formatter) {
value = default_formatter(value, row, column, data);
if (data && data.indent == 0.0) {
value = $(`<span>${value}</span>`);
var $value = $(value).css("font-weight", "bold");
value = $value.wrap("<p></p>").parent().html();
}
return value;
},
}

View File

@@ -41,6 +41,34 @@ def execute(filters=None):
columns = get_columns(group_wise_columns, filters)
if filters.group_by == 'Invoice':
get_data_when_grouped_by_invoice(columns, gross_profit_data, filters, group_wise_columns, data)
else:
get_data_when_not_grouped_by_invoice(gross_profit_data, filters, group_wise_columns, data)
return columns, data
def get_data_when_grouped_by_invoice(columns, gross_profit_data, filters, group_wise_columns, data):
column_names = get_column_names()
# to display item as Item Code: Item Name
columns[0] = 'Sales Invoice:Link/Item:300'
# removing Item Code and Item Name columns
del columns[4:6]
for src in gross_profit_data.si_list:
row = frappe._dict()
row.indent = src.indent
row.parent_invoice = src.parent_invoice
row.currency = filters.currency
for col in group_wise_columns.get(scrub(filters.group_by)):
row[column_names[col]] = src.get(col)
data.append(row)
def get_data_when_not_grouped_by_invoice(gross_profit_data, filters, group_wise_columns, data):
for idx, src in enumerate(gross_profit_data.grouped_data):
row = []
for col in group_wise_columns.get(scrub(filters.group_by)):
@@ -51,8 +79,6 @@ def execute(filters=None):
row[0] = frappe.bold("Total")
data.append(row)
return columns, data
def get_columns(group_wise_columns, filters):
columns = []
column_map = frappe._dict({
@@ -93,12 +119,38 @@ def get_columns(group_wise_columns, filters):
return columns
def get_column_names():
return frappe._dict({
'parent': 'sales_invoice',
'customer': 'customer',
'customer_group': 'customer_group',
'posting_date': 'posting_date',
'item_code': 'item_code',
'item_name': 'item_name',
'item_group': 'item_group',
'brand': 'brand',
'description': 'description',
'warehouse': 'warehouse',
'qty': 'qty',
'base_rate': 'avg._selling_rate',
'buying_rate': 'valuation_rate',
'base_amount': 'selling_amount',
'buying_amount': 'buying_amount',
'gross_profit': 'gross_profit',
'gross_profit_percent': 'gross_profit_%',
'project': 'project'
})
class GrossProfitGenerator(object):
def __init__(self, filters=None):
self.data = []
self.average_buying_rate = {}
self.filters = frappe._dict(filters)
self.load_invoice_items()
if filters.group_by == 'Invoice':
self.group_items_by_invoice()
self.load_stock_ledger_entries()
self.load_product_bundle()
self.load_non_stock_items()
@@ -112,7 +164,12 @@ class GrossProfitGenerator(object):
self.currency_precision = cint(frappe.db.get_default("currency_precision")) or 3
self.float_precision = cint(frappe.db.get_default("float_precision")) or 2
for row in self.si_list:
grouped_by_invoice = True if self.filters.get("group_by") == "Invoice" else False
if grouped_by_invoice:
buying_amount = 0
for row in reversed(self.si_list):
if self.skip_row(row, self.product_bundles):
continue
@@ -134,12 +191,20 @@ class GrossProfitGenerator(object):
row.buying_amount = flt(self.get_buying_amount(row, row.item_code),
self.currency_precision)
if grouped_by_invoice:
if row.indent == 1.0:
buying_amount += row.buying_amount
elif row.indent == 0.0:
row.buying_amount = buying_amount
buying_amount = 0
# get buying rate
if row.qty:
row.buying_rate = flt(row.buying_amount / row.qty, self.float_precision)
row.base_rate = flt(row.base_amount / row.qty, self.float_precision)
else:
row.buying_rate, row.base_rate = 0.0, 0.0
if self.is_not_invoice_row(row):
row.buying_rate, row.base_rate = 0.0, 0.0
# calculate gross profit
row.gross_profit = flt(row.base_amount - row.buying_amount, self.currency_precision)
@@ -185,14 +250,17 @@ class GrossProfitGenerator(object):
for returned_item_row in returned_item_rows:
row.qty += returned_item_row.qty
row.base_amount += flt(returned_item_row.base_amount, self.currency_precision)
row.buying_amount = flt(row.qty * row.buying_rate, self.currency_precision)
if row.qty or row.base_amount:
row.buying_amount = flt(flt(row.qty) * flt(row.buying_rate), self.currency_precision)
if (flt(row.qty) or row.base_amount) and self.is_not_invoice_row(row):
row = self.set_average_rate(row)
self.grouped_data.append(row)
self.add_to_totals(row)
self.set_average_gross_profit(self.totals)
self.grouped_data.append(self.totals)
def is_not_invoice_row(self, row):
return (self.filters.get("group_by") == "Invoice" and row.indent != 0.0) or self.filters.get("group_by") != "Invoice"
def set_average_rate(self, new_row):
self.set_average_gross_profit(new_row)
new_row.buying_rate = flt(new_row.buying_amount / new_row.qty, self.float_precision) if new_row.qty else 0
@@ -356,6 +424,109 @@ class GrossProfitGenerator(object):
.format(conditions=conditions, sales_person_cols=sales_person_cols,
sales_team_table=sales_team_table, match_cond = get_match_cond('Sales Invoice')), self.filters, as_dict=1)
def group_items_by_invoice(self):
"""
Turns list of Sales Invoice Items to a tree of Sales Invoices with their Items as children.
"""
parents = []
for row in self.si_list:
if row.parent not in parents:
parents.append(row.parent)
parents_index = 0
for index, row in enumerate(self.si_list):
if parents_index < len(parents) and row.parent == parents[parents_index]:
invoice = self.get_invoice_row(row)
self.si_list.insert(index, invoice)
parents_index += 1
else:
# skipping the bundle items rows
if not row.indent:
row.indent = 1.0
row.parent_invoice = row.parent
row.parent = row.item_code
if frappe.db.exists('Product Bundle', row.item_code):
self.add_bundle_items(row, index)
def get_invoice_row(self, row):
return frappe._dict({
'parent_invoice': "",
'indent': 0.0,
'parent': row.parent,
'posting_date': row.posting_date,
'posting_time': row.posting_time,
'project': row.project,
'update_stock': row.update_stock,
'customer': row.customer,
'customer_group': row.customer_group,
'item_code': None,
'item_name': None,
'description': None,
'warehouse': None,
'item_group': None,
'brand': None,
'dn_detail': None,
'delivery_note': None,
'qty': None,
'item_row': None,
'is_return': row.is_return,
'cost_center': row.cost_center,
'base_net_amount': frappe.db.get_value('Sales Invoice', row.parent, 'base_net_total')
})
def add_bundle_items(self, product_bundle, index):
bundle_items = self.get_bundle_items(product_bundle)
for i, item in enumerate(bundle_items):
bundle_item = self.get_bundle_item_row(product_bundle, item)
self.si_list.insert((index+i+1), bundle_item)
def get_bundle_items(self, product_bundle):
return frappe.get_all(
'Product Bundle Item',
filters = {
'parent': product_bundle.item_code
},
fields = ['item_code', 'qty']
)
def get_bundle_item_row(self, product_bundle, item):
item_name, description, item_group, brand = self.get_bundle_item_details(item.item_code)
return frappe._dict({
'parent_invoice': product_bundle.item_code,
'indent': product_bundle.indent + 1,
'parent': item.item_code,
'posting_date': product_bundle.posting_date,
'posting_time': product_bundle.posting_time,
'project': product_bundle.project,
'customer': product_bundle.customer,
'customer_group': product_bundle.customer_group,
'item_code': item.item_code,
'item_name': item_name,
'description': description,
'warehouse': product_bundle.warehouse,
'item_group': item_group,
'brand': brand,
'dn_detail': product_bundle.dn_detail,
'delivery_note': product_bundle.delivery_note,
'qty': (flt(product_bundle.qty) * flt(item.qty)),
'item_row': None,
'is_return': product_bundle.is_return,
'cost_center': product_bundle.cost_center
})
def get_bundle_item_details(self, item_code):
return frappe.db.get_value(
'Item',
item_code,
['item_name', 'description', 'item_group', 'brand']
)
def load_stock_ledger_entries(self):
res = frappe.db.sql("""select item_code, voucher_type, voucher_no,
voucher_detail_no, stock_value, warehouse, actual_qty as qty

View File

@@ -122,11 +122,6 @@ class Asset(AccountsController):
if self.is_existing_asset:
return
docname = self.purchase_receipt or self.purchase_invoice
if docname:
doctype = 'Purchase Receipt' if self.purchase_receipt else 'Purchase Invoice'
date = frappe.db.get_value(doctype, docname, 'posting_date')
if self.available_for_use_date and getdate(self.available_for_use_date) < getdate(self.purchase_date):
frappe.throw(_("Available-for-use Date should be after purchase date"))
@@ -404,9 +399,10 @@ class Asset(AccountsController):
if accumulated_depreciation_after_full_schedule:
accumulated_depreciation_after_full_schedule = max(accumulated_depreciation_after_full_schedule)
asset_value_after_full_schedule = flt(flt(self.gross_purchase_amount) -
flt(accumulated_depreciation_after_full_schedule),
self.precision('gross_purchase_amount'))
asset_value_after_full_schedule = flt(
flt(self.gross_purchase_amount) -
flt(self.opening_accumulated_depreciation) -
flt(accumulated_depreciation_after_full_schedule), self.precision('gross_purchase_amount'))
if (row.expected_value_after_useful_life and
row.expected_value_after_useful_life < asset_value_after_full_schedule):

View File

@@ -0,0 +1,8 @@
## ERPNext Version 12.26.0 Release Notes
### Fixes & Enhancements
- Make Gross Profit Report more readable ([#27124](https://github.com/frappe/erpnext/pull/27124))
- Set item uom as stock_uom if it isn't set ([#27623](https://github.com/frappe/erpnext/pull/27623))
- Adding empty row on new maintenance visit ([#27626](https://github.com/frappe/erpnext/pull/27626))
- Employee Leave Balance report should only consider ledgers of transaction type Leave Allocation ([#28017](https://github.com/frappe/erpnext/pull/28017))
- Validate if item exists on uploading items in stock reco ([#27538](https://github.com/frappe/erpnext/pull/27538))

View File

@@ -83,10 +83,8 @@ def add_bank_accounts(response, bank, company):
if not acc_subtype:
add_account_subtype(account["subtype"])
existing_bank_account = frappe.db.exists("Bank Account", {
'account_name': account["name"],
'bank': bank["bank_name"]
})
bank_account_name = "{} - {}".format(account["name"], bank["bank_name"])
existing_bank_account = frappe.db.exists("Bank Account", bank_account_name)
if not existing_bank_account:
try:
@@ -198,6 +196,7 @@ def get_transactions(bank, bank_account=None, start_date=None, end_date=None):
plaid = PlaidConnector(access_token)
transactions = []
try:
transactions = plaid.get_transactions(start_date=start_date, end_date=end_date, account_id=account_id)
except ItemError as e:
@@ -206,7 +205,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) + " "
frappe.log_error(msg, title=_("Plaid Link Refresh Required"))
return transactions or []
return transactions
def new_bank_transaction(transaction):

View File

@@ -18,5 +18,8 @@ frappe.ui.form.on('Shopify Log', {
})
}).addClass('btn-primary');
}
let app_link = "<a href='https://frappecloud.com/marketplace/apps/ecommerce-integrations' target='_blank'>Ecommerce Integrations</a>"
frm.dashboard.add_comment(__("Shopify Integration will be removed from ERPNext in Version 14. Please install {0} app to continue using it.", [app_link]), "yellow", true);
}
});

View File

@@ -36,6 +36,10 @@ frappe.ui.form.on("Shopify Settings", "refresh", function(frm){
frm.toggle_reqd("delivery_note_series", frm.doc.sync_delivery_note);
}
let app_link = "<a href='https://frappecloud.com/marketplace/apps/ecommerce-integrations' target='_blank'>Ecommerce Integrations</a>"
frm.dashboard.add_comment(__("Shopify Integration will be removed from ERPNext in Version 14. Please install {0} app to continue using it.", [app_link]), "yellow", true);
})
$.extend(erpnext_integrations.shopify_settings, {

View File

@@ -6,16 +6,10 @@ frappe.ui.form.on('Salary Component', {
frm.set_query("default_account", "accounts", function(doc, cdt, cdn) {
var d = locals[cdt][cdn];
var root_type = "Liability";
if (frm.doc.type == "Deduction") {
root_type = "Expense";
}
return {
filters: {
"is_group": 0,
"company": d.company,
"root_type": root_type
"company": d.company
}
};
});

View File

@@ -124,11 +124,12 @@ def get_allocated_and_expired_leaves(records, from_date, to_date):
def get_leave_ledger_entries(from_date, to_date, employee, leave_type):
records= frappe.db.sql("""
SELECT
employee, leave_type, from_date, to_date, leaves, transaction_name, transaction_type
is_carry_forward, is_expired
employee, leave_type, from_date, to_date, leaves, transaction_name,
transaction_type, is_carry_forward, is_expired
FROM `tabLeave Ledger Entry`
WHERE employee=%(employee)s AND leave_type=%(leave_type)s
AND docstatus=1
AND transaction_type = 'Leave Allocation'
AND (from_date between %(from_date)s AND %(to_date)s
OR to_date between %(from_date)s AND %(to_date)s
OR (from_date < %(from_date)s AND to_date > %(to_date)s))

View File

@@ -27,6 +27,9 @@ frappe.ui.form.on('Maintenance Visit', {
if (frm.doc.__islocal) {
frm.set_value({mntc_date: frappe.datetime.get_today()});
}
if (frm.doc.purposes.length && frm.doc.purposes[0].item_name == undefined) {
frm.clear_table("purposes");
}
},
customer: function(frm) {
erpnext.utils.get_party_details(frm);

View File

@@ -686,4 +686,4 @@ erpnext.patches.v12_0.purchase_receipt_status
erpnext.patches.v12_0.add_company_link_to_einvoice_settings
erpnext.patches.v12_0.add_document_type_field_for_italy_einvoicing
erpnext.patches.v12_0.create_taxable_value_field_in_purchase_invoice
erpnext.patches.v12_0.show_einvoice_irn_cancelled_field
erpnext.patches.v12_0.show_einvoice_irn_cancelled_field

View File

@@ -6,6 +6,9 @@ import frappe
from frappe.model.utils.rename_field import rename_field
def execute():
# updating column value to handle field change from Data to Currency
frappe.db.sql("update `tabBOM` set base_scrap_material_cost = '0' where trim(coalesce(base_scrap_material_cost, ''))= ''")
for doctype in ['BOM Explosion Item', 'BOM Item', 'Work Order Item', 'Item']:
if frappe.db.has_column(doctype, 'allow_transfer_for_manufacture'):
if doctype != 'Item':
@@ -26,4 +29,4 @@ def execute():
else:
frappe.db.sql(""" UPDATE `tab%s`
SET transfer_material_against = 'Work Order'
WHERE docstatus < 2""" % (doctype))
WHERE docstatus < 2""" % (doctype))

View File

@@ -86,6 +86,10 @@ class Project(Document):
if self.sales_order:
frappe.db.set_value("Sales Order", self.sales_order, "project", self.name)
def on_trash(self):
for so in frappe.get_all("Sales Order", {"project": self.name}, ["name"]):
frappe.db.set_value("Sales Order", so.get('name'), "project", "")
def update_percent_complete(self):
if self.percent_complete_method == "Manual":
if self.status == "Completed":

View File

@@ -8,7 +8,8 @@ test_records = frappe.get_test_records('Project')
test_ignore = ["Sales Order"]
from erpnext.projects.doctype.project_template.test_project_template import get_project_template, make_project_template
from erpnext.projects.doctype.project.project import set_project_status
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
from frappe.utils import getdate
@@ -32,6 +33,21 @@ class TestProject(unittest.TestCase):
self.assertEqual(task4.subject, 'Task 4')
self.assertEqual(getdate(task4.exp_end_date), getdate('2019-01-06'))
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 = get_project_template()

View File

@@ -36,7 +36,8 @@
"fieldname": "qty",
"fieldtype": "Float",
"in_list_view": 1,
"label": "Qty"
"label": "Qty",
"reqd": 1
},
{
"fieldname": "picked_qty",
@@ -180,7 +181,7 @@
],
"istable": 1,
"links": [],
"modified": "2020-03-13 19:08:21.995986",
"modified": "2021-09-28 12:02:16.923056",
"modified_by": "Administrator",
"module": "Stock",
"name": "Pick List Item",
@@ -190,4 +191,4 @@
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
}
}

View File

@@ -282,6 +282,12 @@ frappe.ui.form.on('Stock Entry', {
frm.trigger("setup_quality_inspection");
},
before_save: function(frm) {
frm.doc.items.forEach((item) => {
item.uom = item.uom || item.stock_uom;
})
},
purpose: function(frm) {
frm.trigger('validate_purpose_consumption');
frm.fields_dict.items.grid.refresh();

View File

@@ -537,6 +537,11 @@ def get_stock_balance_for(item_code, warehouse,
item_dict = frappe.db.get_value("Item", item_code,
["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 = ""
with_serial_no = True if item_dict.get("has_serial_no") else False
data = get_stock_balance(item_code, warehouse, posting_date, posting_time,