Merge branch 'develop' into multiple-shifts

This commit is contained in:
Rucha Mahabal 2022-04-07 18:07:39 +05:30 committed by GitHub
commit 7ba66b0320
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
26 changed files with 502 additions and 145 deletions

View File

@ -224,10 +224,7 @@ frappe.ui.form.on('Payment Entry', {
(frm.doc.total_allocated_amount > party_amount)));
frm.toggle_display("set_exchange_gain_loss",
(frm.doc.paid_amount && frm.doc.received_amount && frm.doc.difference_amount &&
((frm.doc.paid_from_account_currency != company_currency ||
frm.doc.paid_to_account_currency != company_currency) &&
frm.doc.paid_from_account_currency != frm.doc.paid_to_account_currency)));
frm.doc.paid_amount && frm.doc.received_amount && frm.doc.difference_amount);
frm.refresh_fields();
},

View File

@ -35,10 +35,11 @@ class PricingRule(Document):
self.margin_rate_or_amount = 0.0
def validate_duplicate_apply_on(self):
field = apply_on_dict.get(self.apply_on)
values = [d.get(frappe.scrub(self.apply_on)) for d in self.get(field) if field]
if len(values) != len(set(values)):
frappe.throw(_("Duplicate {0} found in the table").format(self.apply_on))
if self.apply_on != "Transaction":
field = apply_on_dict.get(self.apply_on)
values = [d.get(frappe.scrub(self.apply_on)) for d in self.get(field) if field]
if len(values) != len(set(values)):
frappe.throw(_("Duplicate {0} found in the table").format(self.apply_on))
def validate_mandatory(self):
for apply_on, field in apply_on_dict.items():

View File

@ -124,11 +124,10 @@ def get_columns(invoice_list, additional_table_columns):
_("Purchase Receipt") + ":Link/Purchase Receipt:100",
{"fieldname": "currency", "label": _("Currency"), "fieldtype": "Data", "width": 80},
]
expense_accounts = (
tax_accounts
) = (
expense_columns
) = tax_columns = unrealized_profit_loss_accounts = unrealized_profit_loss_account_columns = []
expense_accounts = []
tax_accounts = []
unrealized_profit_loss_accounts = []
if invoice_list:
expense_accounts = frappe.db.sql_list(
@ -163,10 +162,11 @@ def get_columns(invoice_list, additional_table_columns):
unrealized_profit_loss_account_columns = [
(account + ":Currency/currency:120") for account in unrealized_profit_loss_accounts
]
for account in tax_accounts:
if account not in expense_accounts:
tax_columns.append(account + ":Currency/currency:120")
tax_columns = [
(account + ":Currency/currency:120")
for account in tax_accounts
if account not in expense_accounts
]
columns = (
columns

View File

@ -126,7 +126,8 @@ class Opportunity(TransactionBase):
def declare_enquiry_lost(self, lost_reasons_list, competitors, detailed_reason=None):
if not self.has_active_quotation():
self.status = "Lost"
self.lost_reasons = self.competitors = []
self.lost_reasons = []
self.competitors = []
if detailed_reason:
self.order_lost_reason = detailed_reason

View File

@ -1,9 +1,9 @@
# Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
import json
from itertools import groupby
import frappe
import pandas
from frappe import _
from frappe.utils import flt
@ -101,18 +101,19 @@ class OpportunitySummaryBySalesStage(object):
self.convert_to_base_currency()
dataframe = pandas.DataFrame.from_records(self.query_result)
dataframe.replace(to_replace=[None], value="Not Assigned", inplace=True)
result = dataframe.groupby(["sales_stage", based_on], as_index=False)["amount"].sum()
for row in self.query_result:
if not row.get(based_on):
row[based_on] = "Not Assigned"
self.grouped_data = []
for i in range(len(result["amount"])):
grouping_key = lambda o: (o["sales_stage"], o[based_on]) # noqa
for (sales_stage, _based_on), rows in groupby(self.query_result, grouping_key):
self.grouped_data.append(
{
"sales_stage": result["sales_stage"][i],
based_on: result[based_on][i],
"amount": result["amount"][i],
"sales_stage": sales_stage,
based_on: _based_on,
"amount": sum(flt(r["amount"]) for r in rows),
}
)

View File

@ -3,9 +3,9 @@
import json
from datetime import date
from itertools import groupby
import frappe
import pandas
from dateutil.relativedelta import relativedelta
from frappe import _
from frappe.utils import cint, flt
@ -109,18 +109,15 @@ class SalesPipelineAnalytics(object):
self.convert_to_base_currency()
dataframe = pandas.DataFrame.from_records(self.query_result)
dataframe.replace(to_replace=[None], value="Not Assigned", inplace=True)
result = dataframe.groupby([self.pipeline_by, self.period_by], as_index=False)["amount"].sum()
self.grouped_data = []
for i in range(len(result["amount"])):
grouping_key = lambda o: (o.get(self.pipeline_by) or "Not Assigned", o[self.period_by]) # noqa
for (pipeline_by, period_by), rows in groupby(self.query_result, grouping_key):
self.grouped_data.append(
{
self.pipeline_by: result[self.pipeline_by][i],
self.period_by: result[self.period_by][i],
"amount": result["amount"][i],
self.pipeline_by: pipeline_by,
self.period_by: period_by,
"amount": sum(flt(r["amount"]) for r in rows),
}
)

View File

@ -34,6 +34,15 @@ frappe.ui.form.on("Leave Allocation", {
});
}
}
// make new leaves allocated field read only if allocation is created via leave policy assignment
// and leave type is earned leave, since these leaves would be allocated via the scheduler
if (frm.doc.leave_policy_assignment) {
frappe.db.get_value("Leave Type", frm.doc.leave_type, "is_earned_leave", (r) => {
if (r && cint(r.is_earned_leave))
frm.set_df_property("new_leaves_allocated", "read_only", 1);
});
}
},
expire_allocation: function(frm) {

View File

@ -237,7 +237,7 @@
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
"modified": "2022-01-18 19:15:53.262536",
"modified": "2022-04-07 09:50:33.145825",
"modified_by": "Administrator",
"module": "HR",
"name": "Leave Allocation",
@ -281,5 +281,6 @@
"sort_order": "DESC",
"states": [],
"timeline_field": "employee",
"title_field": "employee_name"
"title_field": "employee_name",
"track_changes": 1
}

View File

@ -353,6 +353,17 @@ def update_previous_leave_allocation(
allocation.db_set("total_leaves_allocated", new_allocation, update_modified=False)
create_additional_leave_ledger_entry(allocation, earned_leaves, today_date)
if e_leave_type.based_on_date_of_joining:
text = _("allocated {0} leave(s) via scheduler on {1} based on the date of joining").format(
frappe.bold(earned_leaves), frappe.bold(formatdate(today_date))
)
else:
text = _("allocated {0} leave(s) via scheduler on {1}").format(
frappe.bold(earned_leaves), frappe.bold(formatdate(today_date))
)
allocation.add_comment(comment_type="Info", text=text)
def get_monthly_earned_leave(annual_leaves, frequency, rounding):
earned_leaves = 0.0

View File

@ -584,9 +584,10 @@ def regenerate_repayment_schedule(loan, cancel=0):
balance_amount / len(loan_doc.get("repayment_schedule")) - accrued_entries
)
else:
if not cancel:
repayment_period = loan_doc.repayment_periods - accrued_entries
if not cancel and repayment_period > 0:
monthly_repayment_amount = get_monthly_repayment_amount(
balance_amount, loan_doc.rate_of_interest, loan_doc.repayment_periods - accrued_entries
balance_amount, loan_doc.rate_of_interest, repayment_period
)
else:
monthly_repayment_amount = last_repayment_amount

View File

@ -28,12 +28,12 @@ frappe.ui.form.on('Job Card', {
frappe.flags.resume_job = 0;
let has_items = frm.doc.items && frm.doc.items.length;
if (frm.doc.__onload.work_order_closed) {
if (!frm.is_new() && frm.doc.__onload.work_order_closed) {
frm.disable_save();
return;
}
if (!frm.doc.__islocal && has_items && frm.doc.docstatus < 2) {
if (!frm.is_new() && has_items && frm.doc.docstatus < 2) {
let to_request = frm.doc.for_quantity > frm.doc.transferred_qty;
let excess_transfer_allowed = frm.doc.__onload.job_card_excess_transfer;

View File

@ -20,7 +20,7 @@ def execute():
"""
UPDATE `tab{doctype}`
SET is_cancelled = 0
where is_cancelled in ('', NULL, 'No')""".format(
where is_cancelled in ('', 'No') or is_cancelled is NULL""".format(
doctype=doctype
)
)

View File

@ -10,7 +10,7 @@ def execute():
"""
UPDATE `tab{doctype}`
SET is_subcontracted = 0
where is_subcontracted in ('', NULL, 'No')""".format(
where is_subcontracted in ('', 'No') or is_subcontracted is null""".format(
doctype=doctype
)
)

View File

@ -609,8 +609,8 @@ function check_can_calculate_pending_qty(me) {
&& erpnext.stock.bom
&& erpnext.stock.bom.name === doc.bom_no;
const itemChecks = !!item
&& !item.allow_alternative_item
&& erpnext.stock.bom && erpnext.stock.items
&& !item.original_item
&& erpnext.stock.bom && erpnext.stock.bom.items
&& (item.item_code in erpnext.stock.bom.items);
return docChecks && itemChecks;
}

View File

@ -100,7 +100,8 @@ class Customer(TransactionBase):
@frappe.whitelist()
def get_customer_group_details(self):
doc = frappe.get_doc("Customer Group", self.customer_group)
self.accounts = self.credit_limits = []
self.accounts = []
self.credit_limits = []
self.payment_terms = self.default_price_list = ""
tables = [["accounts", "account"], ["credit_limits", "credit_limit"]]

View File

@ -45,7 +45,8 @@ class TestCustomer(FrappeTestCase):
c_doc.customer_name = "Testing Customer"
c_doc.customer_group = "_Testing Customer Group"
c_doc.payment_terms = c_doc.default_price_list = ""
c_doc.accounts = c_doc.credit_limits = []
c_doc.accounts = []
c_doc.credit_limits = []
c_doc.insert()
c_doc.get_customer_group_details()
self.assertEqual(c_doc.payment_terms, "_Test Payment Term Template 3")

View File

@ -1,10 +1,11 @@
# Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and Contributors
# License: GNU General Public License v3. See license.txt
from itertools import groupby
import frappe
import pandas as pd
from frappe import _
from frappe.utils import flt
from erpnext.accounts.report.utils import convert
@ -89,28 +90,21 @@ def get_opp_by_lead_source(from_date, to_date, company):
for x in opportunities
]
df = (
pd.DataFrame(cp_opportunities)
.groupby(["source", "sales_stage"], as_index=False)
.agg({"compound_amount": "sum"})
)
summary = {}
sales_stages = set()
group_key = lambda o: (o["source"], o["sales_stage"]) # noqa
for (source, sales_stage), rows in groupby(cp_opportunities, group_key):
summary.setdefault(source, {})[sales_stage] = sum(r["compound_amount"] for r in rows)
sales_stages.add(sales_stage)
result = {}
result["labels"] = list(set(df.source.values))
result["datasets"] = []
for s in set(df.sales_stage.values):
result["datasets"].append(
{"name": s, "values": [0] * len(result["labels"]), "chartType": "bar"}
)
for row in df.itertuples():
source_index = result["labels"].index(row.source)
for dataset in result["datasets"]:
if dataset["name"] == row.sales_stage:
dataset["values"][source_index] = row.compound_amount
pivot_table = []
for sales_stage in sales_stages:
row = []
for source, sales_stage_values in summary.items():
row.append(flt(sales_stage_values.get(sales_stage)))
pivot_table.append({"chartType": "bar", "name": sales_stage, "values": row})
result = {"datasets": pivot_table, "labels": list(summary.keys())}
return result
else:
@ -148,20 +142,14 @@ def get_pipeline_data(from_date, to_date, company):
for x in opportunities
]
df = (
pd.DataFrame(cp_opportunities)
.groupby(["sales_stage"], as_index=True)
.agg({"compound_amount": "sum"})
.to_dict()
)
result = {}
result["labels"] = df["compound_amount"].keys()
result["datasets"] = []
result["datasets"].append(
{"name": _("Total Amount"), "values": df["compound_amount"].values(), "chartType": "bar"}
)
summary = {}
for sales_stage, rows in groupby(cp_opportunities, lambda o: o["sales_stage"]):
summary[sales_stage] = sum(flt(r["compound_amount"]) for r in rows)
result = {
"labels": list(summary.keys()),
"datasets": [{"name": _("Total Amount"), "values": list(summary.values()), "chartType": "bar"}],
}
return result
else:

View File

@ -27,28 +27,55 @@ function get_filters() {
"default": frappe.datetime.get_today()
},
{
"fieldname":"sales_order",
"label": __("Sales Order"),
"fieldtype": "MultiSelectList",
"fieldname":"customer_group",
"label": __("Customer Group"),
"fieldtype": "Link",
"width": 100,
"options": "Sales Order",
"get_data": function(txt) {
return frappe.db.get_link_options("Sales Order", txt, this.filters());
},
"filters": () => {
return {
docstatus: 1,
payment_terms_template: ['not in', ['']],
company: frappe.query_report.get_filter_value("company"),
transaction_date: ['between', [frappe.query_report.get_filter_value("period_start_date"), frappe.query_report.get_filter_value("period_end_date")]]
"options": "Customer Group",
},
{
"fieldname":"customer",
"label": __("Customer"),
"fieldtype": "Link",
"width": 100,
"options": "Customer",
"get_query": () => {
var customer_group = frappe.query_report.get_filter_value('customer_group');
return{
"query": "erpnext.selling.report.payment_terms_status_for_sales_order.payment_terms_status_for_sales_order.get_customers_or_items",
"filters": [
['Customer', 'disabled', '=', '0'],
['Customer Group','name', '=', customer_group]
]
}
}
},
{
"fieldname":"item_group",
"label": __("Item Group"),
"fieldtype": "Link",
"width": 100,
"options": "Item Group",
},
{
"fieldname":"item",
"label": __("Item"),
"fieldtype": "Link",
"width": 100,
"options": "Item",
"get_query": () => {
var item_group = frappe.query_report.get_filter_value('item_group');
return{
"query": "erpnext.selling.report.payment_terms_status_for_sales_order.payment_terms_status_for_sales_order.get_customers_or_items",
"filters": [
['Item', 'disabled', '=', '0'],
['Item Group','name', '=', item_group]
]
}
},
on_change: function(){
frappe.query_report.refresh();
}
}
]
return filters;
}

View File

@ -3,7 +3,7 @@
import frappe
from frappe import _, qb, query_builder
from frappe.query_builder import functions
from frappe.query_builder import Criterion, functions
def get_columns():
@ -14,6 +14,12 @@ def get_columns():
"fieldtype": "Link",
"options": "Sales Order",
},
{
"label": _("Customer"),
"fieldname": "customer",
"fieldtype": "Link",
"options": "Customer",
},
{
"label": _("Posting Date"),
"fieldname": "submitted",
@ -67,6 +73,55 @@ def get_columns():
return columns
def get_descendants_of(doctype, group_name):
group_doc = qb.DocType(doctype)
# get lft and rgt of group node
lft, rgt = (
qb.from_(group_doc).select(group_doc.lft, group_doc.rgt).where(group_doc.name == group_name)
).run()[0]
# get all children of group node
query = (
qb.from_(group_doc).select(group_doc.name).where((group_doc.lft >= lft) & (group_doc.rgt <= rgt))
)
child_nodes = []
for x in query.run():
child_nodes.append(x[0])
return child_nodes
@frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs
def get_customers_or_items(doctype, txt, searchfield, start, page_len, filters):
filter_list = []
if isinstance(filters, list):
for item in filters:
if item[0] == doctype:
filter_list.append(item)
elif item[0] == "Customer Group":
if item[3] != "":
filter_list.append(
[doctype, "customer_group", "in", get_descendants_of("Customer Group", item[3])]
)
elif item[0] == "Item Group":
if item[3] != "":
filter_list.append([doctype, "item_group", "in", get_descendants_of("Item Group", item[3])])
if searchfield and txt:
filter_list.append([doctype, searchfield, "like", "%%%s%%" % txt])
return frappe.desk.reportview.execute(
doctype,
filters=filter_list,
fields=["name", "customer_group"] if doctype == "Customer" else ["name", "item_group"],
limit_start=start,
limit_page_length=page_len,
as_list=True,
)
def get_conditions(filters):
"""
Convert filter options to conditions used in query
@ -79,11 +134,37 @@ def get_conditions(filters):
conditions.start_date = filters.period_start_date or frappe.utils.add_months(
conditions.end_date, -1
)
conditions.sales_order = filters.sales_order or []
return conditions
def build_filter_criterions(filters):
filters = frappe._dict(filters) if filters else frappe._dict({})
qb_criterions = []
if filters.customer_group:
qb_criterions.append(
qb.DocType("Sales Order").customer_group.isin(
get_descendants_of("Customer Group", filters.customer_group)
)
)
if filters.customer:
qb_criterions.append(qb.DocType("Sales Order").customer == filters.customer)
if filters.item_group:
qb_criterions.append(
qb.DocType("Sales Order Item").item_group.isin(
get_descendants_of("Item Group", filters.item_group)
)
)
if filters.item:
qb_criterions.append(qb.DocType("Sales Order Item").item_code == filters.item)
return qb_criterions
def get_so_with_invoices(filters):
"""
Get Sales Order with payment terms template with their associated Invoices
@ -92,16 +173,23 @@ def get_so_with_invoices(filters):
so = qb.DocType("Sales Order")
ps = qb.DocType("Payment Schedule")
soi = qb.DocType("Sales Order Item")
conditions = get_conditions(filters)
filter_criterions = build_filter_criterions(filters)
datediff = query_builder.CustomFunction("DATEDIFF", ["cur_date", "due_date"])
ifelse = query_builder.CustomFunction("IF", ["condition", "then", "else"])
conditions = get_conditions(filters)
query_so = (
qb.from_(so)
.join(soi)
.on(soi.parent == so.name)
.join(ps)
.on(ps.parent == so.name)
.select(
so.name,
so.customer,
so.transaction_date.as_("submitted"),
ifelse(datediff(ps.due_date, functions.CurDate()) < 0, "Overdue", "Unpaid").as_("status"),
ps.payment_term,
@ -117,12 +205,10 @@ def get_so_with_invoices(filters):
& (so.company == conditions.company)
& (so.transaction_date[conditions.start_date : conditions.end_date])
)
.where(Criterion.all(filter_criterions))
.orderby(so.name, so.transaction_date, ps.due_date)
)
if conditions.sales_order != []:
query_so = query_so.where(so.name.isin(conditions.sales_order))
sorders = query_so.run(as_dict=True)
invoices = []

View File

@ -11,10 +11,13 @@ from erpnext.selling.report.payment_terms_status_for_sales_order.payment_terms_s
)
from erpnext.stock.doctype.item.test_item import create_item
test_dependencies = ["Sales Order", "Item", "Sales Invoice", "Payment Terms Template"]
test_dependencies = ["Sales Order", "Item", "Sales Invoice", "Payment Terms Template", "Customer"]
class TestPaymentTermsStatusForSalesOrder(FrappeTestCase):
def tearDown(self):
frappe.db.rollback()
def create_payment_terms_template(self):
# create template for 50-50 payments
template = None
@ -48,9 +51,9 @@ class TestPaymentTermsStatusForSalesOrder(FrappeTestCase):
template.insert()
self.template = template
def test_payment_terms_status(self):
def test_01_payment_terms_status(self):
self.create_payment_terms_template()
item = create_item(item_code="_Test Excavator", is_stock_item=0)
item = create_item(item_code="_Test Excavator 1", is_stock_item=0)
so = make_sales_order(
transaction_date="2021-06-15",
delivery_date=add_days("2021-06-15", -30),
@ -78,13 +81,14 @@ class TestPaymentTermsStatusForSalesOrder(FrappeTestCase):
"company": "_Test Company",
"period_start_date": "2021-06-01",
"period_end_date": "2021-06-30",
"sales_order": [so.name],
"item": item.item_code,
}
)
expected_value = [
{
"name": so.name,
"customer": so.customer,
"submitted": datetime.date(2021, 6, 15),
"status": "Completed",
"payment_term": None,
@ -98,6 +102,7 @@ class TestPaymentTermsStatusForSalesOrder(FrappeTestCase):
},
{
"name": so.name,
"customer": so.customer,
"submitted": datetime.date(2021, 6, 15),
"status": "Partly Paid",
"payment_term": None,
@ -132,11 +137,11 @@ class TestPaymentTermsStatusForSalesOrder(FrappeTestCase):
)
doc.insert()
def test_alternate_currency(self):
def test_02_alternate_currency(self):
transaction_date = "2021-06-15"
self.create_payment_terms_template()
self.create_exchange_rate(transaction_date)
item = create_item(item_code="_Test Excavator", is_stock_item=0)
item = create_item(item_code="_Test Excavator 2", is_stock_item=0)
so = make_sales_order(
transaction_date=transaction_date,
currency="USD",
@ -166,7 +171,7 @@ class TestPaymentTermsStatusForSalesOrder(FrappeTestCase):
"company": "_Test Company",
"period_start_date": "2021-06-01",
"period_end_date": "2021-06-30",
"sales_order": [so.name],
"item": item.item_code,
}
)
@ -174,6 +179,7 @@ class TestPaymentTermsStatusForSalesOrder(FrappeTestCase):
expected_value = [
{
"name": so.name,
"customer": so.customer,
"submitted": datetime.date(2021, 6, 15),
"status": "Completed",
"payment_term": None,
@ -187,6 +193,7 @@ class TestPaymentTermsStatusForSalesOrder(FrappeTestCase):
},
{
"name": so.name,
"customer": so.customer,
"submitted": datetime.date(2021, 6, 15),
"status": "Partly Paid",
"payment_term": None,
@ -200,3 +207,134 @@ class TestPaymentTermsStatusForSalesOrder(FrappeTestCase):
},
]
self.assertEqual(data, expected_value)
def test_03_group_filters(self):
transaction_date = "2021-06-15"
self.create_payment_terms_template()
item1 = create_item(item_code="_Test Excavator 1", is_stock_item=0)
item1.item_group = "Products"
item1.save()
so1 = make_sales_order(
transaction_date=transaction_date,
delivery_date=add_days(transaction_date, -30),
item=item1.item_code,
qty=1,
rate=1000000,
do_not_save=True,
)
so1.po_no = ""
so1.taxes_and_charges = ""
so1.taxes = ""
so1.payment_terms_template = self.template.name
so1.save()
so1.submit()
item2 = create_item(item_code="_Test Steel", is_stock_item=0)
item2.item_group = "Raw Material"
item2.save()
so2 = make_sales_order(
customer="_Test Customer 1",
transaction_date=transaction_date,
delivery_date=add_days(transaction_date, -30),
item=item2.item_code,
qty=100,
rate=1000,
do_not_save=True,
)
so2.po_no = ""
so2.taxes_and_charges = ""
so2.taxes = ""
so2.payment_terms_template = self.template.name
so2.save()
so2.submit()
base_filters = {
"company": "_Test Company",
"period_start_date": "2021-06-01",
"period_end_date": "2021-06-30",
}
expected_value_so1 = [
{
"name": so1.name,
"customer": so1.customer,
"submitted": datetime.date(2021, 6, 15),
"status": "Overdue",
"payment_term": None,
"description": "_Test 50-50",
"due_date": datetime.date(2021, 6, 30),
"invoice_portion": 50.0,
"currency": "INR",
"base_payment_amount": 500000.0,
"paid_amount": 0.0,
"invoices": "",
},
{
"name": so1.name,
"customer": so1.customer,
"submitted": datetime.date(2021, 6, 15),
"status": "Overdue",
"payment_term": None,
"description": "_Test 50-50",
"due_date": datetime.date(2021, 7, 15),
"invoice_portion": 50.0,
"currency": "INR",
"base_payment_amount": 500000.0,
"paid_amount": 0.0,
"invoices": "",
},
]
expected_value_so2 = [
{
"name": so2.name,
"customer": so2.customer,
"submitted": datetime.date(2021, 6, 15),
"status": "Overdue",
"payment_term": None,
"description": "_Test 50-50",
"due_date": datetime.date(2021, 6, 30),
"invoice_portion": 50.0,
"currency": "INR",
"base_payment_amount": 50000.0,
"paid_amount": 0.0,
"invoices": "",
},
{
"name": so2.name,
"customer": so2.customer,
"submitted": datetime.date(2021, 6, 15),
"status": "Overdue",
"payment_term": None,
"description": "_Test 50-50",
"due_date": datetime.date(2021, 7, 15),
"invoice_portion": 50.0,
"currency": "INR",
"base_payment_amount": 50000.0,
"paid_amount": 0.0,
"invoices": "",
},
]
group_filters = [
{"customer_group": "All Customer Groups"},
{"item_group": "All Item Groups"},
{"item_group": "Products"},
{"item_group": "Raw Material"},
]
expected_values_for_group_filters = [
expected_value_so1 + expected_value_so2,
expected_value_so1 + expected_value_so2,
expected_value_so1,
expected_value_so2,
]
for idx, g in enumerate(group_filters, 0):
# build filter
filters = frappe._dict({}).update(base_filters).update(g)
with self.subTest(filters=filters):
columns, data, message, chart = execute(filters)
self.assertEqual(data, expected_values_for_group_filters[idx])

View File

@ -3,7 +3,7 @@
import copy
import json
from typing import List
from typing import Dict, List, Optional
import frappe
from frappe import _
@ -18,6 +18,7 @@ from frappe.utils import (
now_datetime,
nowtime,
strip,
strip_html,
)
from frappe.utils.html_utils import clean_html
@ -69,10 +70,6 @@ class Item(Document):
self.item_code = strip(self.item_code)
self.name = self.item_code
def before_insert(self):
if not self.description:
self.description = self.item_name
def after_insert(self):
"""set opening stock and item price"""
if self.standard_rate:
@ -86,7 +83,7 @@ class Item(Document):
if not self.item_name:
self.item_name = self.item_code
if not self.description:
if not strip_html(cstr(self.description)).strip():
self.description = self.item_name
self.validate_uom()
@ -890,25 +887,38 @@ class Item(Document):
if self.is_new():
return
fields = ("has_serial_no", "is_stock_item", "valuation_method", "has_batch_no")
restricted_fields = ("has_serial_no", "is_stock_item", "valuation_method", "has_batch_no")
values = frappe.db.get_value("Item", self.name, restricted_fields, as_dict=True)
if not values:
return
values = frappe.db.get_value("Item", self.name, fields, as_dict=True)
if not values.get("valuation_method") and self.get("valuation_method"):
values["valuation_method"] = (
frappe.db.get_single_value("Stock Settings", "valuation_method") or "FIFO"
)
if values:
for field in fields:
if cstr(self.get(field)) != cstr(values.get(field)):
if self.check_if_linked_document_exists(field):
frappe.throw(
_(
"As there are existing transactions against item {0}, you can not change the value of {1}"
).format(self.name, frappe.bold(self.meta.get_label(field)))
)
changed_fields = [
field for field in restricted_fields if cstr(self.get(field)) != cstr(values.get(field))
]
if not changed_fields:
return
def check_if_linked_document_exists(self, field):
if linked_doc := self._get_linked_submitted_documents(changed_fields):
changed_field_labels = [frappe.bold(self.meta.get_label(f)) for f in changed_fields]
msg = _(
"As there are existing submitted transactions against item {0}, you can not change the value of {1}."
).format(self.name, ", ".join(changed_field_labels))
if linked_doc and isinstance(linked_doc, dict):
msg += "<br>"
msg += _("Example of a linked document: {0}").format(
frappe.get_desk_link(linked_doc.doctype, linked_doc.docname)
)
frappe.throw(msg, title=_("Linked with submitted documents"))
def _get_linked_submitted_documents(self, changed_fields: List[str]) -> Optional[Dict[str, str]]:
linked_doctypes = [
"Delivery Note Item",
"Sales Invoice Item",
@ -921,7 +931,7 @@ class Item(Document):
# For "Is Stock Item", following doctypes is important
# because reserved_qty, ordered_qty and requested_qty updated from these doctypes
if field == "is_stock_item":
if "is_stock_item" in changed_fields:
linked_doctypes += [
"Sales Order Item",
"Purchase Order Item",
@ -940,11 +950,21 @@ class Item(Document):
"Sales Invoice Item",
):
# If Invoice has Stock impact, only then consider it.
if self.stock_ledger_created():
return True
if linked_doc := frappe.db.get_value(
"Stock Ledger Entry",
{"item_code": self.name, "is_cancelled": 0},
["voucher_no as docname", "voucher_type as doctype"],
as_dict=True,
):
return linked_doc
elif frappe.db.get_value(doctype, filters):
return True
elif linked_doc := frappe.db.get_value(
doctype,
filters,
["parent as docname", "parenttype as doctype"],
as_dict=True,
):
return linked_doc
def validate_auto_reorder_enabled_in_stock_settings(self):
if self.reorder_levels:

View File

@ -31,7 +31,7 @@ def get_data():
},
{"label": _("Manufacture"), "items": ["Production Plan", "Work Order", "Item Manufacturer"]},
{"label": _("Traceability"), "items": ["Serial No", "Batch"]},
{"label": _("Move"), "items": ["Stock Entry"]},
{"label": _("Stock Movement"), "items": ["Stock Entry", "Stock Reconciliation"]},
{"label": _("E-commerce"), "items": ["Website Item"]},
],
}

View File

@ -744,6 +744,40 @@ class TestItem(FrappeTestCase):
self.assertTrue(get_data(warehouse="_Test Warehouse - _TC"))
self.assertTrue(get_data(item_group="All Item Groups"))
def test_empty_description(self):
item = make_item(properties={"description": "<p></p>"})
self.assertEqual(item.description, item.item_name)
item.description = ""
item.save()
self.assertEqual(item.description, item.item_name)
def test_item_type_field_change(self):
"""Check if critical fields like `is_stock_item`, `has_batch_no` are not changed if transactions exist."""
from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice
from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note
from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt
from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry
transaction_creators = [
lambda i: make_purchase_receipt(item_code=i),
lambda i: make_purchase_invoice(item_code=i, update_stock=1),
lambda i: make_stock_entry(item_code=i, qty=1, target="_Test Warehouse - _TC"),
lambda i: create_delivery_note(item_code=i),
]
properties = {"has_batch_no": 0, "allow_negative_stock": 1, "valuation_rate": 10}
for transaction_creator in transaction_creators:
item = make_item(properties=properties)
transaction = transaction_creator(item.name)
item.has_batch_no = 1
self.assertRaises(frappe.ValidationError, item.save)
transaction.cancel()
# should be allowed now
item.reload()
item.has_batch_no = 1
item.save()
def set_item_variant_settings(fields):
doc = frappe.get_doc("Item Variant Settings")

View File

@ -38,6 +38,16 @@ class TestWarehouse(FrappeTestCase):
self.assertEqual(p_warehouse.name, child_warehouse.parent_warehouse)
self.assertEqual(child_warehouse.is_group, 0)
def test_naming(self):
company = "Wind Power LLC"
warehouse_name = "Named Warehouse - WP"
wh = frappe.get_doc(doctype="Warehouse", warehouse_name=warehouse_name, company=company).insert()
self.assertEqual(wh.name, warehouse_name)
warehouse_name = "Unnamed Warehouse"
wh = frappe.get_doc(doctype="Warehouse", warehouse_name=warehouse_name, company=company).insert()
self.assertIn(warehouse_name, wh.name)
def test_unlinking_warehouse_from_item_defaults(self):
company = "_Test Company"

View File

@ -21,8 +21,9 @@ class Warehouse(NestedSet):
suffix = " - " + frappe.get_cached_value("Company", self.company, "abbr")
if not self.warehouse_name.endswith(suffix):
self.name = self.warehouse_name + suffix
else:
self.name = self.warehouse_name
return
self.name = self.warehouse_name
def onload(self):
"""load account name for General Ledger Report"""

View File

@ -285,7 +285,7 @@ Asset scrapped via Journal Entry {0},Actif mis au rebut via Écriture de Journal
"Asset {0} cannot be scrapped, as it is already {1}","L'actif {0} ne peut pas être mis au rebut, car il est déjà {1}",
Asset {0} does not belong to company {1},L'actif {0} ne fait pas partie à la société {1},
Asset {0} must be submitted,L'actif {0} doit être soumis,
Assets,Les atouts,
Assets,Actifs - Immo.,
Assign,Assigner,
Assign Salary Structure,Affecter la structure salariale,
Assign To,Attribuer À,
@ -1211,7 +1211,7 @@ Hello,Bonjour,
Help Results for,Aide Résultats pour,
High,Haut,
High Sensitivity,Haute sensibilité,
Hold,Tenir,
Hold,Mettre en attente,
Hold Invoice,Facture en attente,
Holiday,Vacances,
Holiday List,Liste de vacances,
@ -4240,7 +4240,7 @@ For Default Supplier (Optional),Pour le fournisseur par défaut (facultatif),
From date cannot be greater than To date,La Date Initiale ne peut pas être postérieure à la Date Finale,
Group by,Grouper Par,
In stock,En stock,
Item name,Nom de l'article,
Item name,Libellé de l'article,
Loan amount is mandatory,Le montant du prêt est obligatoire,
Minimum Qty,Quantité minimum,
More details,Plus de détails,
@ -5473,7 +5473,7 @@ Percentage you are allowed to transfer more against the quantity ordered. For ex
PUR-ORD-.YYYY.-,PUR-ORD-.YYYY.-,
Get Items from Open Material Requests,Obtenir des Articles de Demandes Matérielles Ouvertes,
Fetch items based on Default Supplier.,Récupérez les articles en fonction du fournisseur par défaut.,
Required By,Requis Par,
Required By,Requis pour le,
Order Confirmation No,No de confirmation de commande,
Order Confirmation Date,Date de confirmation de la commande,
Customer Mobile No,N° de Portable du Client,
@ -7223,8 +7223,8 @@ Basic Rate (Company Currency),Taux de Base (Devise de la Société ),
Scrap %,% de Rebut,
Original Item,Article original,
BOM Operation,Opération LDM,
Operation Time ,Moment de l&#39;opération,
In minutes,En quelques minutes,
Operation Time ,Durée de l&#39;opération,
In minutes,En minutes,
Batch Size,Taille du lot,
Base Hour Rate(Company Currency),Taux Horaire de Base (Devise de la Société),
Operating Cost(Company Currency),Coût d'Exploitation (Devise Société),
@ -9267,7 +9267,7 @@ Sales Order Analysis,Analyse des commandes clients,
Amount Delivered,Montant livré,
Delay (in Days),Retard (en jours),
Group by Sales Order,Regrouper par commande client,
Sales Value,La valeur des ventes,
Sales Value,La valeur des ventes,
Stock Qty vs Serial No Count,Quantité de stock vs numéro de série,
Serial No Count,Numéro de série,
Work Order Summary,Résumé de l&#39;ordre de travail,
@ -9647,7 +9647,7 @@ Allow Multiple Sales Orders Against a Customer's Purchase Order,Autoriser plusie
Validate Selling Price for Item Against Purchase Rate or Valuation Rate,Valider le prix de vente de l&#39;article par rapport au taux d&#39;achat ou au taux de valorisation,
Hide Customer's Tax ID from Sales Transactions,Masquer le numéro d&#39;identification fiscale du client dans les transactions de vente,
"The percentage you are allowed to receive or deliver more against the quantity ordered. For example, if you have ordered 100 units, and your Allowance is 10%, then you are allowed to receive 110 units.","Le pourcentage que vous êtes autorisé à recevoir ou à livrer plus par rapport à la quantité commandée. Par exemple, si vous avez commandé 100 unités et que votre allocation est de 10%, vous êtes autorisé à recevoir 110 unités.",
Action If Quality Inspection Is Not Submitted,Action si l&#39;inspection de la qualité n&#39;est pas soumise,
Action If Quality Inspection Is Not Submitted,Action si l&#39;inspection qualité n&#39;est pas soumise,
Auto Insert Price List Rate If Missing,Taux de liste de prix d&#39;insertion automatique s&#39;il est manquant,
Automatically Set Serial Nos Based on FIFO,Définir automatiquement les numéros de série en fonction de FIFO,
Set Qty in Transactions Based on Serial No Input,Définir la quantité dans les transactions en fonction du numéro de série,
@ -9838,3 +9838,35 @@ Enable European Access,Activer l&#39;accès européen,
Creating Purchase Order ...,Création d&#39;une commande d&#39;achat ...,
"Select a Supplier from the Default Suppliers of the items below. On selection, a Purchase Order will be made against items belonging to the selected Supplier only.","Sélectionnez un fournisseur parmi les fournisseurs par défaut des articles ci-dessous. Lors de la sélection, un bon de commande sera effectué contre des articles appartenant uniquement au fournisseur sélectionné.",
Row #{}: You must select {} serial numbers for item {}.,Ligne n ° {}: vous devez sélectionner {} numéros de série pour l&#39;article {}.,
Update Rate as per Last Purchase,Mettre à jour avec les derniers prix d'achats
Company Shipping Address,Adresse d&#39;expédition
Shipping Address Details,Détail d&#39;adresse d&#39;expédition
Company Billing Address,Adresse de la société de facturation
Supplier Address Details,
Bank Reconciliation Tool,Outil de réconcialiation d&#39;écritures bancaires
Supplier Contact,Contact fournisseur
Subcontracting,Sous traitance
Order Status,Statut de la commande
Build,Personnalisations avancées
Dispatch Address Name,Adresse de livraison intermédiaire
Amount Eligible for Commission,Montant éligible à comission
Grant Commission,Eligible aux commissions
Stock Transactions Settings, Paramétre des transactions
Role Allowed to Over Deliver/Receive, Rôle autorisé à dépasser cette limite
Users with this role are allowed to over deliver/receive against orders above the allowance percentage,Rôle Utilisateur qui sont autorisé à livrée/commandé au-delà de la limite
Over Transfer Allowance,Autorisation de limite de transfert
Quality Inspection Settings,Paramétre de l&#39;inspection qualité
Action If Quality Inspection Is Rejected,Action si l'inspection qualité est rejetée
Disable Serial No And Batch Selector,Désactiver le sélecteur de numéro de lot/série
Is Rate Adjustment Entry (Debit Note),Est un justement du prix de la note de débit
Issue a debit note with 0 qty against an existing Sales Invoice,Creer une note de débit avec une quatité à O pour la facture
Control Historical Stock Transactions,Controle de l&#39;historique des stransaction de stock
No stock transactions can be created or modified before this date.,Aucune transaction ne peux être créée ou modifié avant cette date.
Stock transactions that are older than the mentioned days cannot be modified.,Les transactions de stock plus ancienne que le nombre de jours ci-dessus ne peuvent être modifiées
Role Allowed to Create/Edit Back-dated Transactions,Rôle autorisé à créer et modifier des transactions anti-datée
"If mentioned, the system will allow only the users with this Role to create or modify any stock transaction earlier than the latest stock transaction for a specific item and warehouse. If set as blank, it allows all users to create/edit back-dated transactions.","LEs utilisateur de ce role pourront creer et modifier des transactions dans le passé. Si vide tout les utilisateurs pourrons le faire"
Auto Insert Item Price If Missing,Création du prix de l'article dans les listes de prix si abscent
Update Existing Price List Rate,Mise a jour automatique du prix dans les listes de prix
Show Barcode Field in Stock Transactions,Afficher le champ Code Barre dans les transactions de stock
Convert Item Description to Clean HTML in Transactions,Convertir les descriptions d'articles en HTML valide lors des transactions
Have Default Naming Series for Batch ID?,Nom de série par défaut pour les Lots ou Séries

Can't render this file because it is too large.