Merge pull request #31050 from ankush/v13_release

chore: release
This commit is contained in:
Ankush Menat
2022-05-17 13:09:46 +05:30
committed by GitHub
79 changed files with 145920 additions and 434 deletions

View File

@@ -234,17 +234,19 @@ def get_checks_for_pl_and_bs_accounts():
return dimensions
def get_dimension_with_children(doctype, dimension):
def get_dimension_with_children(doctype, dimensions):
if isinstance(dimension, list):
dimension = dimension[0]
if isinstance(dimensions, str):
dimensions = [dimensions]
all_dimensions = []
lft, rgt = frappe.db.get_value(doctype, dimension, ["lft", "rgt"])
children = frappe.get_all(
doctype, filters={"lft": [">=", lft], "rgt": ["<=", rgt]}, order_by="lft"
)
all_dimensions += [c.name for c in children]
for dimension in dimensions:
lft, rgt = frappe.db.get_value(doctype, dimension, ["lft", "rgt"])
children = frappe.get_all(
doctype, filters={"lft": [">=", lft], "rgt": ["<=", rgt]}, order_by="lft"
)
all_dimensions += [c.name for c in children]
return all_dimensions

View File

@@ -357,6 +357,12 @@ class PaymentEntry(AccountsController):
)
)
if ref_doc.doctype == "Purchase Invoice" and ref_doc.get("on_hold"):
frappe.throw(
_("{0} {1} is on hold").format(d.reference_doctype, d.reference_name),
title=_("Invalid Invoice"),
)
if ref_doc.docstatus != 1:
frappe.throw(_("{0} {1} must be submitted").format(d.reference_doctype, d.reference_name))

View File

@@ -743,6 +743,21 @@ class TestPaymentEntry(unittest.TestCase):
flt(payment_entry.total_taxes_and_charges, 2), flt(10 / payment_entry.target_exchange_rate, 2)
)
def test_payment_entry_against_onhold_purchase_invoice(self):
pi = make_purchase_invoice()
pe = get_payment_entry("Purchase Invoice", pi.name, bank_account="_Test Bank USD - _TC")
pe.reference_no = "1"
pe.reference_date = "2016-01-01"
# block invoice after creating payment entry
# since `get_payment_entry` will not attach blocked invoice to payment
pi.block_invoice()
with self.assertRaises(frappe.ValidationError) as err:
pe.save()
self.assertTrue("is on hold" in str(err.exception).lower())
def create_payment_entry(**args):
payment_entry = frappe.new_doc("Payment Entry")

View File

@@ -3086,6 +3086,39 @@ class TestSalesInvoice(unittest.TestCase):
si.reload()
self.assertTrue(si.items[0].serial_no)
def test_sales_invoice_with_disabled_account(self):
try:
account = frappe.get_doc("Account", "VAT 5% - _TC")
account.disabled = 1
account.save()
si = create_sales_invoice(do_not_save=True)
si.posting_date = add_days(getdate(), 1)
si.taxes = []
si.append(
"taxes",
{
"charge_type": "On Net Total",
"account_head": "VAT 5% - _TC",
"cost_center": "Main - _TC",
"description": "VAT @ 5.0",
"rate": 9,
},
)
si.save()
with self.assertRaises(frappe.ValidationError) as err:
si.submit()
self.assertTrue(
"Cannot create accounting entries against disabled accounts" in str(err.exception)
)
finally:
account.disabled = 0
account.save()
def test_gain_loss_with_advance_entry(self):
from erpnext.accounts.doctype.journal_entry.test_journal_entry import make_journal_entry

View File

@@ -29,6 +29,7 @@ def make_gl_entries(
if gl_map:
if not cancel:
validate_accounting_period(gl_map)
validate_disabled_accounts(gl_map)
gl_map = process_gl_map(gl_map, merge_entries)
if gl_map and len(gl_map) > 1:
save_entries(gl_map, adv_adj, update_outstanding, from_repost)
@@ -43,6 +44,26 @@ def make_gl_entries(
make_reverse_gl_entries(gl_map, adv_adj=adv_adj, update_outstanding=update_outstanding)
def validate_disabled_accounts(gl_map):
accounts = [d.account for d in gl_map if d.account]
Account = frappe.qb.DocType("Account")
disabled_accounts = (
frappe.qb.from_(Account)
.where(Account.name.isin(accounts) & Account.disabled == 1)
.select(Account.name, Account.disabled)
).run(as_dict=True)
if disabled_accounts:
account_list = "<br>"
account_list += ", ".join([frappe.bold(d.name) for d in disabled_accounts])
frappe.throw(
_("Cannot create accounting entries against disabled accounts: {0}").format(account_list),
title=_("Disabled Account Selected"),
)
def validate_accounting_period(gl_map):
accounting_periods = frappe.db.sql(
""" SELECT

View File

@@ -539,7 +539,7 @@ def get_additional_conditions(from_date, ignore_closing_entries, filters):
)
additional_conditions.append("{0} in %({0})s".format(dimension.fieldname))
else:
additional_conditions.append("{0} in (%({0})s)".format(dimension.fieldname))
additional_conditions.append("{0} in %({0})s".format(dimension.fieldname))
return " and {}".format(" and ".join(additional_conditions)) if additional_conditions else ""

View File

@@ -318,7 +318,7 @@ def get_conditions(filters):
)
conditions.append("{0} in %({0})s".format(dimension.fieldname))
else:
conditions.append("{0} in (%({0})s)".format(dimension.fieldname))
conditions.append("{0} in %({0})s".format(dimension.fieldname))
return "and {}".format(" and ".join(conditions)) if conditions else ""

View File

@@ -237,7 +237,7 @@ def get_conditions(filters):
else:
conditions += (
common_condition
+ "and ifnull(`tabPurchase Invoice Item`.{0}, '') in (%({0})s))".format(dimension.fieldname)
+ "and ifnull(`tabPurchase Invoice Item`.{0}, '') in %({0})s)".format(dimension.fieldname)
)
return conditions

View File

@@ -405,7 +405,7 @@ def get_conditions(filters):
else:
conditions += (
common_condition
+ "and ifnull(`tabSales Invoice Item`.{0}, '') in (%({0})s))".format(dimension.fieldname)
+ "and ifnull(`tabSales Invoice Item`.{0}, '') in %({0})s)".format(dimension.fieldname)
)
return conditions

View File

@@ -188,9 +188,9 @@ def get_rootwise_opening_balances(filters, report_type):
filters[dimension.fieldname] = get_dimension_with_children(
dimension.document_type, filters.get(dimension.fieldname)
)
additional_conditions += "and {0} in %({0})s".format(dimension.fieldname)
additional_conditions += " and {0} in %({0})s".format(dimension.fieldname)
else:
additional_conditions += "and {0} in (%({0})s)".format(dimension.fieldname)
additional_conditions += " and {0} in %({0})s".format(dimension.fieldname)
query_filters.update({dimension.fieldname: filters.get(dimension.fieldname)})

View File

@@ -353,15 +353,15 @@ class Asset(AccountsController):
if self.allow_monthly_depreciation:
# month range is 1 to 12
# In pro rata case, for first and last depreciation, month range would be different
month_range = (
months
if (has_pro_rata and n == 0)
or (has_pro_rata and n == cint(number_of_pending_depreciations) - 1)
else finance_book.frequency_of_depreciation
)
if (has_pro_rata and n == 0 and not self.number_of_depreciations_booked) or (
has_pro_rata and n == cint(number_of_pending_depreciations) - 1
):
month_range = months
else:
month_range = finance_book.frequency_of_depreciation
for r in range(month_range):
if has_pro_rata and n == 0:
if has_pro_rata and n == 0 and not self.number_of_depreciations_booked:
# For first entry of monthly depr
if r == 0:
days_until_first_depr = date_diff(monthly_schedule_date, self.available_for_use_date) + 1

View File

@@ -637,6 +637,8 @@ def make_rm_stock_entry(purchase_order, rm_items):
}
}
stock_entry.add_to_stock_entry_detail(items_dict)
stock_entry.set_missing_values()
return stock_entry.as_dict()
else:
frappe.throw(_("No Items selected for transfer"))
@@ -724,7 +726,7 @@ def make_return_stock_entry_for_subcontract(available_materials, po_doc, po_deta
add_items_in_ste(ste_doc, value, value.qty, po_details)
ste_doc.set_stock_entry_type()
ste_doc.calculate_rate_and_amount()
ste_doc.set_missing_values()
return ste_doc

View File

@@ -31,7 +31,7 @@ frappe.ui.form.on("Request for Quotation",{
if (frm.doc.docstatus === 1) {
frm.add_custom_button(__('Supplier Quotation'),
function(){ frm.trigger("make_suppplier_quotation") }, __("Create"));
function(){ frm.trigger("make_supplier_quotation") }, __("Create"));
frm.add_custom_button(__("Send Emails to Suppliers"), function() {
@@ -87,16 +87,24 @@ frappe.ui.form.on("Request for Quotation",{
},
make_suppplier_quotation: function(frm) {
make_supplier_quotation: function(frm) {
var doc = frm.doc;
var dialog = new frappe.ui.Dialog({
title: __("Create Supplier Quotation"),
fields: [
{ "fieldtype": "Select", "label": __("Supplier"),
{ "fieldtype": "Link",
"label": __("Supplier"),
"fieldname": "supplier",
"options": doc.suppliers.map(d => d.supplier),
"options": 'Supplier',
"reqd": 1,
"default": doc.suppliers.length === 1 ? doc.suppliers[0].supplier_name : "" },
get_query: () => {
return {
filters: [
["Supplier", "name", "in", frm.doc.suppliers.map((row) => {return row.supplier;})]
]
}
}
}
],
primary_action_label: __("Create"),
primary_action: (args) => {

View File

@@ -32,7 +32,9 @@
"terms",
"printing_settings",
"select_print_heading",
"letter_head"
"letter_head",
"more_info",
"opportunity"
],
"fields": [
{
@@ -193,6 +195,23 @@
"options": "Letter Head",
"print_hide": 1
},
{
"collapsible": 1,
"fieldname": "more_info",
"fieldtype": "Section Break",
"label": "More Information",
"oldfieldtype": "Section Break",
"options": "fa fa-file-text",
"print_hide": 1
},
{
"fieldname": "opportunity",
"fieldtype": "Link",
"label": "Opportunity",
"options": "Opportunity",
"print_hide": 1,
"read_only": 1
},
{
"fieldname": "status",
"fieldtype": "Select",
@@ -258,7 +277,7 @@
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
"modified": "2021-11-24 17:47:49.909000",
"modified": "2022-04-06 17:47:49.909000",
"modified_by": "Administrator",
"module": "Buying",
"name": "Request for Quotation",
@@ -327,4 +346,4 @@
"show_name_in_global_search": 1,
"sort_field": "modified",
"sort_order": "DESC"
}
}

View File

@@ -149,6 +149,7 @@ class AccountsController(TransactionBase):
self.validate_inter_company_reference()
self.disable_pricing_rule_on_internal_transfer()
self.set_incoming_rate()
if self.meta.get_field("currency"):
@@ -383,6 +384,14 @@ class AccountsController(TransactionBase):
msg += _("Please create purchase from internal sale or delivery document itself")
frappe.throw(msg, title=_("Internal Sales Reference Missing"))
def disable_pricing_rule_on_internal_transfer(self):
if not self.get("ignore_pricing_rule") and self.is_internal_transfer():
self.ignore_pricing_rule = 1
frappe.msgprint(
_("Disabled pricing rules since this {} is an internal transfer").format(self.doctype),
alert=1,
)
def validate_due_date(self):
if self.get("is_pos"):
return
@@ -1117,11 +1126,10 @@ class AccountsController(TransactionBase):
{
"account": item.discount_account,
"against": supplier_or_customer,
dr_or_cr: flt(discount_amount, item.precision("discount_amount")),
dr_or_cr
+ "_in_account_currency": flt(
dr_or_cr: flt(
discount_amount * self.get("conversion_rate"), item.precision("discount_amount")
),
dr_or_cr + "_in_account_currency": flt(discount_amount, item.precision("discount_amount")),
"cost_center": item.cost_center,
"project": item.project,
},
@@ -1136,11 +1144,11 @@ class AccountsController(TransactionBase):
{
"account": income_or_expense_account,
"against": supplier_or_customer,
rev_dr_cr: flt(discount_amount, item.precision("discount_amount")),
rev_dr_cr
+ "_in_account_currency": flt(
rev_dr_cr: flt(
discount_amount * self.get("conversion_rate"), item.precision("discount_amount")
),
rev_dr_cr
+ "_in_account_currency": flt(discount_amount, item.precision("discount_amount")),
"cost_center": item.cost_center,
"project": item.project or self.project,
},
@@ -1736,6 +1744,8 @@ class AccountsController(TransactionBase):
internal_party_field = "is_internal_customer"
elif self.doctype in ("Purchase Invoice", "Purchase Receipt", "Purchase Order"):
internal_party_field = "is_internal_supplier"
else:
return False
if self.get(internal_party_field) and (self.represents_company == self.company):
return True
@@ -2449,11 +2459,21 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil
parent_doctype, parent_doctype_name, child_doctype, child_docname, item_row
)
def validate_quantity(child_item, d):
if parent_doctype == "Sales Order" and flt(d.get("qty")) < flt(child_item.delivered_qty):
def validate_quantity(child_item, new_data):
if not flt(new_data.get("qty")):
frappe.throw(
_("Row # {0}: Quantity for Item {1} cannot be zero").format(
new_data.get("idx"), frappe.bold(new_data.get("item_code"))
),
title=_("Invalid Qty"),
)
if parent_doctype == "Sales Order" and flt(new_data.get("qty")) < flt(child_item.delivered_qty):
frappe.throw(_("Cannot set quantity less than delivered quantity"))
if parent_doctype == "Purchase Order" and flt(d.get("qty")) < flt(child_item.received_qty):
if parent_doctype == "Purchase Order" and flt(new_data.get("qty")) < flt(
child_item.received_qty
):
frappe.throw(_("Cannot set quantity less than received quantity"))
data = json.loads(trans_items)

View File

@@ -306,14 +306,15 @@ class BuyingController(StockController, Subcontracting):
if self.is_internal_transfer():
if rate != d.rate:
d.rate = rate
d.discount_percentage = 0
d.discount_amount = 0
frappe.msgprint(
_(
"Row {0}: Item rate has been updated as per valuation rate since its an internal stock transfer"
).format(d.idx),
alert=1,
)
d.discount_percentage = 0.0
d.discount_amount = 0.0
d.margin_rate_or_amount = 0.0
def get_supplied_items_cost(self, item_row_id, reset_outgoing_rate=True):
supplied_items_cost = 0.0

View File

@@ -162,6 +162,7 @@ def tax_account_query(doctype, txt, searchfield, start, page_len, filters):
{account_type_condition}
AND is_group = 0
AND company = %(company)s
AND disabled = %(disabled)s
AND (account_currency = %(currency)s or ifnull(account_currency, '') = '')
AND `{searchfield}` LIKE %(txt)s
{mcond}
@@ -175,6 +176,7 @@ def tax_account_query(doctype, txt, searchfield, start, page_len, filters):
dict(
account_types=filters.get("account_type"),
company=filters.get("company"),
disabled=filters.get("disabled", 0),
currency=company_currency,
txt="%{}%".format(txt),
offset=start,

View File

@@ -446,15 +446,16 @@ class SellingController(StockController):
rate = flt(d.incoming_rate * d.conversion_factor, d.precision("rate"))
if d.rate != rate:
d.rate = rate
frappe.msgprint(
_(
"Row {0}: Item rate has been updated as per valuation rate since its an internal stock transfer"
).format(d.idx),
alert=1,
)
d.discount_percentage = 0
d.discount_amount = 0
frappe.msgprint(
_(
"Row {0}: Item rate has been updated as per valuation rate since its an internal stock transfer"
).format(d.idx),
alert=1,
)
d.discount_percentage = 0.0
d.discount_amount = 0.0
d.margin_rate_or_amount = 0.0
elif self.get("return_against"):
# Get incoming rate of return entry from reference document

View File

@@ -8,7 +8,7 @@ import frappe
from frappe import _
from frappe.email.inbox import link_communication_to_document
from frappe.model.mapper import get_mapped_doc
from frappe.utils import cint, cstr, get_fullname
from frappe.utils import cint, get_fullname
from erpnext.accounts.party import get_party_account_currency
from erpnext.setup.utils import get_exchange_rate
@@ -193,20 +193,20 @@ class Opportunity(TransactionBase):
if self.party_name and self.opportunity_from == "Customer":
if self.contact_person:
opts.description = "Contact " + cstr(self.contact_person)
opts.description = f"Contact {self.contact_person}"
else:
opts.description = "Contact customer " + cstr(self.party_name)
opts.description = f"Contact customer {self.party_name}"
elif self.party_name and self.opportunity_from == "Lead":
if self.contact_display:
opts.description = "Contact " + cstr(self.contact_display)
opts.description = f"Contact {self.contact_display}"
else:
opts.description = "Contact lead " + cstr(self.party_name)
opts.description = f"Contact lead {self.party_name}"
opts.subject = opts.description
opts.description += ". By : " + cstr(self.contact_by)
opts.description += f". By : {self.contact_by}"
if self.to_discuss:
opts.description += " To Discuss : " + cstr(self.to_discuss)
opts.description += f" To Discuss : {frappe.render_template(self.to_discuss, {'doc': self})}"
super(Opportunity, self).add_calendar_event(opts, force)

View File

@@ -2,6 +2,6 @@ def get_data():
return {
"fieldname": "opportunity",
"transactions": [
{"items": ["Quotation", "Supplier Quotation"]},
{"items": ["Quotation", "Request for Quotation", "Supplier Quotation"]},
],
}

View File

@@ -4,7 +4,7 @@
import unittest
import frappe
from frappe.utils import random_string, today
from frappe.utils import add_days, now_datetime, random_string, today
from erpnext.crm.doctype.lead.lead import make_customer
from erpnext.crm.doctype.opportunity.opportunity import make_quotation
@@ -58,6 +58,22 @@ class TestOpportunity(unittest.TestCase):
self.assertEqual(opp_doc.opportunity_from, "Customer")
self.assertEqual(opp_doc.party_name, customer.name)
def test_render_template_for_to_discuss(self):
doc = make_opportunity(with_items=0, opportunity_from="Lead")
doc.contact_by = "test@example.com"
doc.contact_date = add_days(today(), days=2)
doc.to_discuss = "{{ doc.name }} test data"
doc.save()
event = frappe.get_all(
"Event Participants",
fields=["parent"],
filters={"reference_doctype": doc.doctype, "reference_docname": doc.name},
)
event_description = frappe.db.get_value("Event", event[0].parent, "description")
self.assertTrue(doc.name in event_description)
def make_opportunity(**args):
args = frappe._dict(args)

View File

@@ -382,6 +382,7 @@ doc_events = {
"validate": [
"erpnext.regional.india.utils.validate_document_name",
"erpnext.regional.india.utils.update_taxable_values",
"erpnext.regional.india.utils.validate_sez_and_export_invoices",
],
},
"POS Invoice": {"on_submit": ["erpnext.regional.saudi_arabia.utils.create_qr_code"]},

View File

@@ -21,6 +21,9 @@ class Attendance(Document):
self.validate_employee_status()
self.check_leave_record()
def on_cancel(self):
self.unlink_attendance_from_checkins()
def validate_attendance_date(self):
date_of_joining = frappe.db.get_value("Employee", self.employee, "date_of_joining")
@@ -102,6 +105,35 @@ class Attendance(Document):
if not emp:
frappe.throw(_("Employee {0} is not active or does not exist").format(self.employee))
def unlink_attendance_from_checkins(self):
from frappe.utils import get_link_to_form
EmployeeCheckin = frappe.qb.DocType("Employee Checkin")
linked_logs = (
frappe.qb.from_(EmployeeCheckin)
.select(EmployeeCheckin.name)
.where(EmployeeCheckin.attendance == self.name)
.for_update()
.run(as_dict=True)
)
if linked_logs:
(
frappe.qb.update(EmployeeCheckin)
.set("attendance", "")
.where(EmployeeCheckin.attendance == self.name)
).run()
frappe.msgprint(
msg=_("Unlinked Attendance record from Employee Checkins: {}").format(
", ".join(get_link_to_form("Employee Checkin", log.name) for log in linked_logs)
),
title=_("Unlinked logs"),
indicator="blue",
is_minimizable=True,
wide=True,
)
@frappe.whitelist()
def get_events(start, end, filters=None):

View File

@@ -65,9 +65,10 @@ frappe.ui.form.on('Employee Advance', {
);
}
if (frm.doc.docstatus === 1 &&
(flt(frm.doc.claimed_amount) < flt(frm.doc.paid_amount) && flt(frm.doc.paid_amount) != flt(frm.doc.return_amount))) {
if (
frm.doc.docstatus === 1
&& (flt(frm.doc.claimed_amount) < flt(frm.doc.paid_amount) - flt(frm.doc.return_amount))
) {
if (frm.doc.repay_unclaimed_amount_from_salary == 0 && frappe.model.can_create("Journal Entry")) {
frm.add_custom_button(__("Return"), function() {
frm.trigger('make_return_entry');

View File

@@ -225,11 +225,15 @@ def calculate_working_hours(logs, check_in_out_type, working_hours_calc_type):
in_log = out_log = None
if not in_log:
in_log = log if log.log_type == "IN" else None
if in_log and not in_time:
in_time = in_log.time
elif not out_log:
out_log = log if log.log_type == "OUT" else None
if in_log and out_log:
out_time = out_log.time
total_hours += time_diff_in_hours(in_log.time, out_log.time)
return total_hours, in_time, out_time

View File

@@ -54,6 +54,17 @@ class TestEmployeeCheckin(unittest.TestCase):
)
self.assertEqual(attendance_count, 1)
def test_unlink_attendance_on_cancellation(self):
employee = make_employee("test_mark_attendance_and_link_log@example.com")
logs = make_n_checkins(employee, 3)
frappe.db.delete("Attendance", {"employee": employee})
attendance = mark_attendance_and_link_log(logs, "Present", nowdate(), 8.2)
attendance.cancel()
linked_logs = frappe.db.get_all("Employee Checkin", {"attendance": attendance.name})
self.assertEquals(len(linked_logs), 0)
def test_calculate_working_hours(self):
check_in_out_type = [
"Alternating entries as IN and OUT during the same shift",
@@ -103,6 +114,11 @@ class TestEmployeeCheckin(unittest.TestCase):
)
self.assertEqual(working_hours, (4.5, logs_type_2[1].time, logs_type_2[-1].time))
working_hours = calculate_working_hours(
[logs_type_2[1], logs_type_2[-1]], check_in_out_type[1], working_hours_calc_type[1]
)
self.assertEqual(working_hours, (5.0, logs_type_2[1].time, logs_type_2[-1].time))
def make_n_checkins(employee, n, hours_to_reverse=1):
logs = [make_checkin(employee, now_datetime() - timedelta(hours=hours_to_reverse, minutes=n + 1))]

View File

@@ -4,21 +4,21 @@
frappe.query_reports["Employee Leave Balance"] = {
"filters": [
{
"fieldname":"from_date",
"fieldname": "from_date",
"label": __("From Date"),
"fieldtype": "Date",
"reqd": 1,
"default": frappe.defaults.get_default("year_start_date")
},
{
"fieldname":"to_date",
"fieldname": "to_date",
"label": __("To Date"),
"fieldtype": "Date",
"reqd": 1,
"default": frappe.defaults.get_default("year_end_date")
},
{
"fieldname":"company",
"fieldname": "company",
"label": __("Company"),
"fieldtype": "Link",
"options": "Company",
@@ -26,16 +26,29 @@ frappe.query_reports["Employee Leave Balance"] = {
"default": frappe.defaults.get_user_default("Company")
},
{
"fieldname":"department",
"fieldname": "department",
"label": __("Department"),
"fieldtype": "Link",
"options": "Department",
},
{
"fieldname":"employee",
"fieldname": "employee",
"label": __("Employee"),
"fieldtype": "Link",
"options": "Employee",
},
{
"fieldname": "employee_status",
"label": __("Employee Status"),
"fieldtype": "Select",
"options": [
"",
{ "value": "Active", "label": __("Active") },
{ "value": "Inactive", "label": __("Inactive") },
{ "value": "Suspended", "label": __("Suspended") },
{ "value": "Left", "label": __("Left") },
],
"default": "Active",
}
],

View File

@@ -168,9 +168,8 @@ def get_opening_balance(
def get_conditions(filters: Filters) -> Dict:
conditions = {
"status": "Active",
}
conditions = {}
if filters.get("employee"):
conditions["name"] = filters.get("employee")
@@ -180,6 +179,9 @@ def get_conditions(filters: Filters) -> Dict:
if filters.get("department"):
conditions["department"] = filters.get("department")
if filters.get("employee_status"):
conditions["status"] = filters.get("employee_status")
return conditions

View File

@@ -207,3 +207,40 @@ class TestEmployeeLeaveBalance(unittest.TestCase):
allocation1.new_leaves_allocated - leave_application.total_leave_days
)
self.assertEqual(report[1][0].opening_balance, opening_balance)
@set_holiday_list("_Test Emp Balance Holiday List", "_Test Company")
def test_employee_status_filter(self):
frappe.get_doc(test_records[0]).insert()
inactive_emp = make_employee("test_emp_status@example.com", company="_Test Company")
allocation = make_allocation_record(
employee=inactive_emp,
from_date=self.year_start,
to_date=self.year_end,
leaves=5,
)
# set employee as inactive
frappe.db.set_value("Employee", inactive_emp, "status", "Inactive")
filters = frappe._dict(
{
"from_date": allocation.from_date,
"to_date": allocation.to_date,
"employee": inactive_emp,
"employee_status": "Active",
}
)
report = execute(filters)
self.assertEqual(len(report[1]), 0)
filters = frappe._dict(
{
"from_date": allocation.from_date,
"to_date": allocation.to_date,
"employee": inactive_emp,
"employee_status": "Inactive",
}
)
report = execute(filters)
self.assertEqual(len(report[1]), 1)

View File

@@ -30,6 +30,19 @@ frappe.query_reports['Employee Leave Balance Summary'] = {
label: __('Department'),
fieldtype: 'Link',
options: 'Department',
},
{
fieldname: "employee_status",
label: __("Employee Status"),
fieldtype: "Select",
options: [
"",
{ "value": "Active", "label": __("Active") },
{ "value": "Inactive", "label": __("Inactive") },
{ "value": "Suspended", "label": __("Suspended") },
{ "value": "Left", "label": __("Left") },
],
default: "Active",
}
]
};

View File

@@ -35,9 +35,10 @@ def get_columns(leave_types):
def get_conditions(filters):
conditions = {
"status": "Active",
"company": filters.company,
}
if filters.get("employee_status"):
conditions.update({"status": filters.get("employee_status")})
if filters.get("department"):
conditions.update({"department": filters.get("department")})
if filters.get("employee"):

View File

@@ -36,7 +36,6 @@ class TestEmployeeLeaveBalance(unittest.TestCase):
frappe.set_user("Administrator")
self.employee_id = make_employee("test_emp_leave_balance@example.com", company="_Test Company")
self.employee_id = make_employee("test_emp_leave_balance@example.com", company="_Test Company")
self.date = getdate()
@@ -146,3 +145,37 @@ class TestEmployeeLeaveBalance(unittest.TestCase):
]
self.assertEqual(report[1], expected_data)
@set_holiday_list("_Test Emp Balance Holiday List", "_Test Company")
def test_employee_status_filter(self):
frappe.get_doc(test_records[0]).insert()
inactive_emp = make_employee("test_emp_status@example.com", company="_Test Company")
allocation = make_allocation_record(
employee=inactive_emp, from_date=self.year_start, to_date=self.year_end
)
# set employee as inactive
frappe.db.set_value("Employee", inactive_emp, "status", "Inactive")
filters = frappe._dict(
{
"date": allocation.from_date,
"company": "_Test Company",
"employee": inactive_emp,
"employee_status": "Active",
}
)
report = execute(filters)
self.assertEqual(len(report[1]), 0)
filters = frappe._dict(
{
"date": allocation.from_date,
"company": "_Test Company",
"employee": inactive_emp,
"employee_status": "Inactive",
}
)
report = execute(filters)
self.assertEqual(len(report[1]), 1)

View File

@@ -265,6 +265,7 @@ class LoanRepayment(AccountsController):
regenerate_repayment_schedule(self.against_loan, cancel)
def allocate_amounts(self, repayment_details):
precision = cint(frappe.db.get_default("currency_precision")) or 2
self.set("repayment_details", [])
self.principal_amount_paid = 0
self.total_penalty_paid = 0
@@ -279,9 +280,9 @@ class LoanRepayment(AccountsController):
if interest_paid > 0:
if self.penalty_amount and interest_paid > self.penalty_amount:
self.total_penalty_paid = self.penalty_amount
self.total_penalty_paid = flt(self.penalty_amount, precision)
elif self.penalty_amount:
self.total_penalty_paid = interest_paid
self.total_penalty_paid = flt(interest_paid, precision)
interest_paid -= self.total_penalty_paid

View File

@@ -764,8 +764,6 @@ def make_stock_entry(source_name, target_doc=None):
pending_fg_qty = flt(source.get("for_quantity", 0)) - flt(source.get("transferred_qty", 0))
target.fg_completed_qty = pending_fg_qty if pending_fg_qty > 0 else 0
target.set_transfer_qty()
target.calculate_rate_and_amount()
target.set_missing_values()
target.set_stock_entry_type()

View File

@@ -363,4 +363,6 @@ erpnext.patches.v13_0.set_return_against_in_pos_invoice_references
erpnext.patches.v13_0.copy_custom_field_filters_to_website_item
erpnext.patches.v13_0.set_available_for_use_date_if_missing
erpnext.patches.v13_0.education_deprecation_warning
erpnext.patches.v13_0.requeue_recoverable_reposts
erpnext.patches.v13_0.create_accounting_dimensions_in_orders
erpnext.patches.v13_0.set_per_billed_in_return_delivery_note

View File

@@ -0,0 +1,21 @@
import frappe
def execute():
recoverable = ("QueryDeadlockError", "QueryTimeoutError", "JobTimeoutException")
failed_reposts = frappe.get_all(
"Repost Item Valuation",
fields=["name", "error_log"],
filters={
"status": "Failed",
"docstatus": 1,
"modified": (">", "2022-04-20"),
"error_log": ("is", "set"),
},
)
for riv in failed_reposts:
for exc in recoverable:
if exc in riv.error_log:
frappe.db.set_value("Repost Item Valuation", riv.name, "status", "Queued")
break

View File

@@ -0,0 +1,29 @@
# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
import frappe
def execute():
dn = frappe.qb.DocType("Delivery Note")
dn_item = frappe.qb.DocType("Delivery Note Item")
dn_list = (
frappe.qb.from_(dn)
.inner_join(dn_item)
.on(dn.name == dn_item.parent)
.select(dn.name)
.where(dn.docstatus == 1)
.where(dn.is_return == 1)
.where(dn.per_billed < 100)
.where(dn_item.returned_qty > 0)
.run(as_dict=True)
)
frappe.qb.update(dn_item).inner_join(dn).on(dn.name == dn_item.parent).set(
dn_item.returned_qty, 0
).where(dn.is_return == 1).where(dn_item.returned_qty > 0).run()
for d in dn_list:
dn_doc = frappe.get_doc("Delivery Note", d.get("name"))
dn_doc.run_method("update_billing_status")

View File

@@ -27,7 +27,8 @@ frappe.ui.form.on(cur_frm.doctype, {
query: "erpnext.controllers.queries.tax_account_query",
filters: {
"account_type": account_type,
"company": doc.company
"company": doc.company,
"disabled": 0
}
}
});

View File

@@ -90,7 +90,7 @@ erpnext.buying.BuyingController = erpnext.TransactionController.extend({
else {
return{
query: "erpnext.controllers.queries.item_query",
filters: { 'supplier': me.frm.doc.supplier, 'is_purchase_item': 1 }
filters: { 'supplier': me.frm.doc.supplier, 'is_purchase_item': 1, 'has_variants': 0}
}
}
});

View File

@@ -1035,12 +1035,11 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({
},
currency: function() {
/* manqala 19/09/2016: let the translation date be whichever of the transaction_date or posting_date is available */
var transaction_date = this.frm.doc.transaction_date || this.frm.doc.posting_date;
/* end manqala */
var me = this;
let transaction_date = this.frm.doc.transaction_date || this.frm.doc.posting_date;
let me = this;
this.set_dynamic_labels();
var company_currency = this.get_company_currency();
let company_currency = this.get_company_currency();
// Added `ignore_price_list` to determine if document is loading after mapping from another doc
if(this.frm.doc.currency && this.frm.doc.currency !== company_currency
&& !(this.frm.doc.__onload && this.frm.doc.__onload.ignore_price_list)) {
@@ -1054,7 +1053,9 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({
}
});
} else {
this.conversion_rate();
// company currency and doc currency is same
// this will prevent unnecessary conversion rate triggers
this.frm.set_value("conversion_rate", 1.0);
}
},
@@ -1496,12 +1497,15 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({
var me = this;
var args = this._get_args(item);
if (!(args.items && args.items.length)) {
if(calculate_taxes_and_totals) me.calculate_taxes_and_totals();
if (calculate_taxes_and_totals) me.calculate_taxes_and_totals();
return;
}
// Target doc created from a mapped doc
if (this.frm.doc.__onload && this.frm.doc.__onload.ignore_price_list) {
// Calculate totals even though pricing rule is not applied.
// `apply_pricing_rule` is triggered due to change in data which most likely contributes to Total.
if (calculate_taxes_and_totals) me.calculate_taxes_and_totals();
return;
}

View File

@@ -125,7 +125,7 @@ $.extend(erpnext.utils, {
},
add_indicator_for_multicompany: function(frm, info) {
frm.dashboard.stats_area.removeClass('hidden');
frm.dashboard.stats_area.show();
frm.dashboard.stats_area_row.addClass('flex');
frm.dashboard.stats_area_row.css('flex-wrap', 'wrap');
@@ -213,8 +213,10 @@ $.extend(erpnext.utils, {
filters.splice(index, 0, {
"fieldname": dimension["fieldname"],
"label": __(dimension["label"]),
"fieldtype": "Link",
"options": dimension["document_type"]
"fieldtype": "MultiSelectList",
get_data: function(txt) {
return frappe.db.get_link_options(dimension["document_type"], txt);
},
});
}
});

View File

@@ -1,4 +1,5 @@
{
"actions": [],
"autoname": "field:hsn_code",
"creation": "2017-06-21 10:48:56.422086",
"doctype": "DocType",
@@ -7,6 +8,7 @@
"field_order": [
"hsn_code",
"description",
"gst_rates",
"taxes"
],
"fields": [
@@ -16,22 +18,37 @@
"in_list_view": 1,
"label": "HSN Code",
"reqd": 1,
"show_days": 1,
"show_seconds": 1,
"unique": 1
},
{
"fieldname": "description",
"fieldtype": "Small Text",
"in_list_view": 1,
"label": "Description"
"label": "Description",
"show_days": 1,
"show_seconds": 1
},
{
"fieldname": "taxes",
"fieldtype": "Table",
"label": "Taxes",
"options": "Item Tax"
"options": "Item Tax",
"show_days": 1,
"show_seconds": 1
},
{
"fieldname": "gst_rates",
"fieldtype": "Table",
"label": "GST Rates",
"options": "HSN Tax Rate",
"show_days": 1,
"show_seconds": 1
}
],
"modified": "2019-11-01 11:18:59.556931",
"links": [],
"modified": "2022-05-11 13:42:27.286643",
"modified_by": "Administrator",
"module": "Regional",
"name": "GST HSN Code",

View File

@@ -3,7 +3,7 @@
frappe.ui.form.on('GST Settings', {
refresh: function(frm) {
frm.add_custom_button('Send GST Update Reminder', () => {
frm.add_custom_button(__('Send GST Update Reminder'), () => {
return new Promise((resolve) => {
return frappe.call({
method: 'erpnext.regional.doctype.gst_settings.gst_settings.send_reminder'
@@ -11,6 +11,12 @@ frappe.ui.form.on('GST Settings', {
});
});
frm.add_custom_button(__('Sync HSN Codes'), () => {
frappe.call({
"method": "erpnext.regional.doctype.gst_settings.gst_settings.update_hsn_codes"
});
});
$(frm.fields_dict.gst_summary.wrapper).empty().html(
`<table class="table table-bordered">
<tbody>

View File

@@ -2,13 +2,14 @@
# For license information, please see license.txt
import json
import os
import frappe
from frappe import _
from frappe.contacts.doctype.contact.contact import get_default_contact
from frappe.model.document import Document
from frappe.utils import date_diff, get_url, nowdate
from frappe.utils import date_diff, flt, get_url, nowdate
class EmailMissing(frappe.ValidationError):
@@ -129,3 +130,31 @@ def _send_gstin_reminder(party_type, party, default_email_id=None, sent_to=None)
)
return email_id
@frappe.whitelist()
def update_hsn_codes():
frappe.enqueue(enqueue_update)
frappe.msgprint(_("HSN/SAC Code sync started, this may take a few minutes..."))
def enqueue_update():
with open(os.path.join(os.path.dirname(__file__), "hsn_code_data.json"), "r") as f:
hsn_codes = json.loads(f.read())
for hsn_code in hsn_codes:
try:
hsn_code_doc = frappe.get_doc("GST HSN Code", hsn_code.get("hsn_code"))
hsn_code_doc.set("gst_rates", [])
for rate in hsn_code.get("gst_rates"):
hsn_code_doc.append(
"gst_rates",
{
"minimum_taxable_value": flt(hsn_code.get("minimum_taxable_value")),
"tax_rate": flt(rate.get("tax_rate")),
},
)
hsn_code_doc.save()
except Exception as e:
pass

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,39 @@
{
"actions": [],
"allow_rename": 1,
"creation": "2022-05-11 13:32:42.534779",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"minimum_taxable_value",
"tax_rate"
],
"fields": [
{
"columns": 2,
"fieldname": "minimum_taxable_value",
"fieldtype": "Currency",
"in_list_view": 1,
"label": "Minimum Taxable Value"
},
{
"columns": 2,
"fieldname": "tax_rate",
"fieldtype": "Float",
"in_list_view": 1,
"label": "Tax Rate"
}
],
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2022-05-15 15:37:56.152470",
"modified_by": "Administrator",
"module": "Regional",
"name": "HSN Tax Rate",
"owner": "Administrator",
"permissions": [],
"sort_field": "modified",
"sort_order": "DESC"
}

View File

@@ -0,0 +1,9 @@
# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
# import frappe
from frappe.model.document import Document
class HSNTaxRate(Document):
pass

View File

@@ -841,6 +841,30 @@ def get_gst_accounts(
return gst_accounts
def validate_sez_and_export_invoices(doc, method):
country = frappe.get_cached_value("Company", doc.company, "country")
if country != "India":
return
if (
doc.get("gst_category") in ("SEZ", "Overseas")
and doc.get("export_type") == "Without Payment of Tax"
):
gst_accounts = get_gst_accounts(doc.company)
for tax in doc.get("taxes"):
for tax in doc.get("taxes"):
if (
tax.account_head
in gst_accounts.get("igst_account", [])
+ gst_accounts.get("sgst_account", [])
+ gst_accounts.get("cgst_account", [])
and tax.tax_amount_after_discount_amount
):
frappe.throw(_("GST cannot be applied on SEZ or Export invoices without payment of tax"))
def validate_reverse_charge_transaction(doc, method):
country = frappe.get_cached_value("Company", doc.company, "country")
@@ -888,6 +912,8 @@ def validate_reverse_charge_transaction(doc, method):
frappe.throw(msg)
doc.eligibility_for_itc = "ITC on Reverse Charge"
def update_itc_availed_fields(doc, method):
country = frappe.get_cached_value("Company", doc.company, "country")

View File

@@ -227,7 +227,10 @@ class Gstr1Report(object):
taxable_value += abs(net_amount)
elif (
not tax_rate
and self.filters.get("type_of_business") == "EXPORT"
and (
self.filters.get("type_of_business") == "EXPORT"
or invoice_details.get("gst_category") == "SEZ"
)
and invoice_details.get("export_type") == "Without Payment of Tax"
):
taxable_value += abs(net_amount)
@@ -329,12 +332,14 @@ class Gstr1Report(object):
def get_invoice_items(self):
self.invoice_items = frappe._dict()
self.item_tax_rate = frappe._dict()
self.item_hsn_map = frappe._dict()
self.nil_exempt_non_gst = {}
# nosemgrep
items = frappe.db.sql(
"""
select item_code, parent, taxable_value, base_net_amount, item_tax_rate, is_nil_exempt,
is_non_gst from `tab%s Item`
gst_hsn_code, is_non_gst from `tab%s Item`
where parent in (%s)
"""
% (self.doctype, ", ".join(["%s"] * len(self.invoices))),
@@ -344,6 +349,7 @@ class Gstr1Report(object):
for d in items:
self.invoice_items.setdefault(d.parent, {}).setdefault(d.item_code, 0.0)
self.item_hsn_map.setdefault(d.item_code, d.gst_hsn_code)
self.invoice_items[d.parent][d.item_code] += d.get("taxable_value", 0) or d.get(
"base_net_amount", 0
)
@@ -368,6 +374,8 @@ class Gstr1Report(object):
self.nil_exempt_non_gst[d.parent][2] += d.get("taxable_value", 0)
def get_items_based_on_tax_rate(self):
hsn_wise_tax_rate = get_hsn_wise_tax_rates()
self.tax_details = frappe.db.sql(
"""
select
@@ -428,7 +436,7 @@ class Gstr1Report(object):
alert=True,
)
# Build itemised tax for export invoices where tax table is blank
# Build itemised tax for export invoices where tax table is blank (Export and SEZ Invoices)
for invoice, items in iteritems(self.invoice_items):
if (
invoice not in self.items_based_on_tax_rate
@@ -436,7 +444,17 @@ class Gstr1Report(object):
and self.invoices.get(invoice, {}).get("export_type") == "Without Payment of Tax"
and self.invoices.get(invoice, {}).get("gst_category") in ("Overseas", "SEZ")
):
self.items_based_on_tax_rate.setdefault(invoice, {}).setdefault(0, items.keys())
self.items_based_on_tax_rate.setdefault(invoice, {})
for item_code in items.keys():
hsn_code = self.item_hsn_map.get(item_code)
tax_rate = 0
taxable_value = items.get(item_code)
for rates in hsn_wise_tax_rate.get(hsn_code):
if taxable_value > rates.get("minimum_taxable_value"):
tax_rate = rates.get("tax_rate")
self.items_based_on_tax_rate[invoice].setdefault(tax_rate, [])
self.items_based_on_tax_rate[invoice][tax_rate].append(item_code)
def get_columns(self):
self.other_columns = []
@@ -729,7 +747,7 @@ def get_json(filters, report_name, data):
elif filters["type_of_business"] == "EXPORT":
for item in report_data[:-1]:
res.setdefault(item["export_type"], []).append(item)
res.setdefault(item["export_type"], {}).setdefault(item["invoice_number"], []).append(item)
out = get_export_json(res)
gst_json["exp"] = out
@@ -919,11 +937,21 @@ def get_export_json(res):
for exp_type in res:
exp_item, inv = {"exp_typ": exp_type, "inv": []}, []
for row in res[exp_type]:
inv_item = get_basic_invoice_detail(row)
inv_item["itms"] = [
{"txval": flt(row["taxable_value"], 2), "rt": row["rate"] or 0, "iamt": 0, "csamt": 0}
]
for number, invoice in iteritems(res[exp_type]):
inv_item = get_basic_invoice_detail(invoice[0])
inv_item["itms"] = []
for item in invoice:
inv_item["itms"].append(
{
"txval": flt(item["taxable_value"], 2),
"rt": flt(item["rate"]),
"iamt": flt((item["taxable_value"] * flt(item["rate"])) / 100.0, 2)
if exp_type != "WOPAY"
else 0,
"csamt": (flt(item.get("cess_amount"), 2) or 0),
}
)
inv.append(inv_item)
@@ -1061,7 +1089,6 @@ def get_rate_and_tax_details(row, gstin):
# calculate tax amount added
tax = flt((row["taxable_value"] * rate) / 100.0, 2)
frappe.errprint([tax, tax / 2])
if row.get("billing_address_gstin") and gstin[0:2] == row["billing_address_gstin"][0:2]:
itm_det.update({"camt": flt(tax / 2.0, 2), "samt": flt(tax / 2.0, 2)})
else:
@@ -1137,3 +1164,26 @@ def get_company_gstins(company):
address_list = [""] + [d.gstin for d in addresses]
return address_list
def get_hsn_wise_tax_rates():
hsn_wise_tax_rate = {}
gst_hsn_code = frappe.qb.DocType("GST HSN Code")
hsn_tax_rates = frappe.qb.DocType("HSN Tax Rate")
hsn_code_data = (
frappe.qb.from_(gst_hsn_code)
.inner_join(hsn_tax_rates)
.on(gst_hsn_code.name == hsn_tax_rates.parent)
.select(gst_hsn_code.hsn_code, hsn_tax_rates.tax_rate, hsn_tax_rates.minimum_taxable_value)
.orderby(hsn_tax_rates.minimum_taxable_value)
.run(as_dict=1)
)
for d in hsn_code_data:
hsn_wise_tax_rate.setdefault(d.hsn_code, [])
hsn_wise_tax_rate[d.hsn_code].append(
{"minimum_taxable_value": d.minimum_taxable_value, "tax_rate": d.tax_rate}
)
return hsn_wise_tax_rate

View File

@@ -222,7 +222,7 @@ def get_merged_data(columns, data):
result = []
for row in data:
key = row[0] + "-" + str(row[4])
key = row[0] + "-" + row[2] + "-" + str(row[4])
merged_hsn_dict.setdefault(key, {})
for i, d in enumerate(columns):
if d["fieldtype"] not in ("Int", "Float", "Currency"):

View File

@@ -369,7 +369,14 @@ def set_credit_limit(customer, company, credit_limit):
customer.credit_limits[-1].db_insert()
def create_internal_customer(customer_name, represents_company, allowed_to_interact_with):
def create_internal_customer(
customer_name=None, represents_company=None, allowed_to_interact_with=None
):
if not customer_name:
customer_name = represents_company
if not allowed_to_interact_with:
allowed_to_interact_with = represents_company
if not frappe.db.exists("Customer", customer_name):
customer = frappe.get_doc(
{

View File

@@ -238,4 +238,5 @@ def get_chart_data(data):
"datasets": [{"name": _(" Total Sales Amount"), "values": datapoints[:30]}],
},
"type": "bar",
"fieldtype": "Currency",
}

View File

@@ -54,4 +54,5 @@ def get_chart_data(data, conditions, filters):
},
"type": "line",
"lineOptions": {"regionFill": 1},
"fieldtype": "Currency",
}

View File

@@ -416,3 +416,8 @@ class Analytics(object):
else:
labels = [d.get("label") for d in self.columns[1 : length - 1]]
self.chart = {"data": {"labels": labels, "datasets": []}, "type": "line"}
if self.filters["value_quantity"] == "Value":
self.chart["fieldtype"] = "Currency"
else:
self.chart["fieldtype"] = "Float"

View File

@@ -53,4 +53,5 @@ def get_chart_data(data, conditions, filters):
},
"type": "line",
"lineOptions": {"regionFill": 1},
"fieldtype": "Currency",
}

View File

@@ -64,7 +64,7 @@ erpnext.selling.SellingController = erpnext.TransactionController.extend({
this.frm.set_query("item_code", "items", function() {
return {
query: "erpnext.controllers.queries.item_query",
filters: {'is_sales_item': 1, 'customer': cur_frm.doc.customer}
filters: {'is_sales_item': 1, 'customer': cur_frm.doc.customer, 'has_variants': 0}
}
});
}

View File

@@ -570,15 +570,12 @@ class TestDeliveryNote(FrappeTestCase):
customer=customer_name,
cost_center="Main - TCP1",
expense_account="Cost of Goods Sold - TCP1",
do_not_submit=True,
qty=5,
rate=500,
warehouse="Stores - TCP1",
target_warehouse=target_warehouse,
)
dn.submit()
# qty after delivery
actual_qty_at_source = get_qty_after_transaction(warehouse="Stores - TCP1")
self.assertEqual(actual_qty_at_source, 475)
@@ -962,6 +959,111 @@ class TestDeliveryNote(FrappeTestCase):
automatically_fetch_payment_terms(enable=0)
def test_returned_qty_in_return_dn(self):
# SO ---> SI ---> DN
# |
# |---> DN(Partial Sales Return) ---> SI(Credit Note)
# |
# |---> DN(Partial Sales Return) ---> SI(Credit Note)
from erpnext.accounts.doctype.sales_invoice.sales_invoice import make_delivery_note
from erpnext.selling.doctype.sales_order.sales_order import make_sales_invoice
so = make_sales_order(qty=10)
si = make_sales_invoice(so.name)
si.insert()
si.submit()
dn = make_delivery_note(si.name)
dn.insert()
dn.submit()
self.assertEqual(dn.items[0].returned_qty, 0)
self.assertEqual(dn.per_billed, 100)
from erpnext.stock.doctype.delivery_note.delivery_note import make_sales_invoice
dn1 = create_delivery_note(is_return=1, return_against=dn.name, qty=-3)
si1 = make_sales_invoice(dn1.name)
si1.insert()
si1.submit()
dn1.reload()
self.assertEqual(dn1.items[0].returned_qty, 0)
self.assertEqual(dn1.per_billed, 100)
dn2 = create_delivery_note(is_return=1, return_against=dn.name, qty=-4)
si2 = make_sales_invoice(dn2.name)
si2.insert()
si2.submit()
dn2.reload()
self.assertEqual(dn2.items[0].returned_qty, 0)
self.assertEqual(dn2.per_billed, 100)
def test_internal_transfer_with_valuation_only(self):
from erpnext.selling.doctype.customer.test_customer import create_internal_customer
item = make_item().name
warehouse = "_Test Warehouse - _TC"
target = "Stores - _TC"
company = "_Test Company"
customer = create_internal_customer(represents_company=company)
rate = 42
# Create item price and pricing rule
frappe.get_doc(
{
"item_code": item,
"price_list": "Standard Selling",
"price_list_rate": 1000,
"doctype": "Item Price",
}
).insert()
frappe.get_doc(
{
"doctype": "Pricing Rule",
"title": frappe.generate_hash(),
"apply_on": "Item Code",
"price_or_product_discount": "Price",
"selling": 1,
"company": company,
"margin_type": "Percentage",
"margin_rate_or_amount": 10,
"apply_discount_on": "Grand Total",
"items": [
{
"item_code": item,
}
],
}
).insert()
make_stock_entry(target=warehouse, qty=5, basic_rate=rate, item_code=item)
dn = create_delivery_note(
item_code=item,
company=company,
customer=customer,
qty=5,
rate=500,
warehouse=warehouse,
target_warehouse=target,
ignore_pricing_rule=0,
do_not_save=True,
do_not_submit=True,
)
self.assertEqual(dn.items[0].rate, 500) # haven't saved yet
dn.save()
self.assertEqual(dn.ignore_pricing_rule, 1)
# rate should reset to incoming rate
self.assertEqual(dn.items[0].rate, rate)
# rate should reset again if discounts are fiddled with
dn.items[0].margin_type = "Amount"
dn.items[0].margin_rate_or_amount = 50
dn.save()
self.assertEqual(dn.items[0].rate, rate)
def create_delivery_note(**args):
dn = frappe.new_doc("Delivery Note")

View File

@@ -737,7 +737,9 @@
"depends_on": "returned_qty",
"fieldname": "returned_qty",
"fieldtype": "Float",
"label": "Returned Qty in Stock UOM"
"label": "Returned Qty in Stock UOM",
"no_copy": 1,
"read_only": 1
},
{
"fieldname": "incoming_rate",
@@ -778,7 +780,7 @@
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2022-03-31 18:36:24.671913",
"modified": "2022-05-02 12:09:39.610075",
"modified_by": "Administrator",
"module": "Stock",
"name": "Delivery Note Item",

View File

@@ -586,8 +586,7 @@ $.extend(erpnext.item, {
["parent","=", d.attribute]
],
fields: ["attribute_value"],
limit_start: 0,
limit_page_length: 500,
limit_page_length: 0,
parent: "Item Attribute",
order_by: "idx"
}

View File

@@ -600,7 +600,7 @@ def make_stock_entry(source_name, target_doc=None):
if source.material_request_type == "Customer Provided":
target.purpose = "Material Receipt"
target.run_method("calculate_rate_and_amount")
target.set_missing_values()
target.set_stock_entry_type()
target.set_job_card_data()

View File

@@ -671,8 +671,7 @@ def create_stock_entry(pick_list):
else:
stock_entry = update_stock_entry_items_with_no_reference(pick_list, stock_entry)
stock_entry.set_actual_qty()
stock_entry.calculate_rate_and_amount()
stock_entry.set_missing_values()
return stock_entry.as_dict()

View File

@@ -993,6 +993,7 @@ def make_stock_entry(source_name, target_doc=None):
def set_missing_values(source, target):
target.stock_entry_type = "Material Transfer"
target.purpose = "Material Transfer"
target.set_missing_values()
doclist = get_mapped_doc(
"Purchase Receipt",

View File

@@ -3,9 +3,11 @@
import frappe
from frappe import _
from frappe.exceptions import QueryDeadlockError, QueryTimeoutError
from frappe.model.document import Document
from frappe.utils import cint, get_link_to_form, get_weekday, now, nowtime
from frappe.utils.user import get_users_with_role
from rq.timeouts import JobTimeoutException
import erpnext
from erpnext.accounts.utils import get_future_stock_vouchers, repost_gle_for_stock_vouchers
@@ -15,6 +17,8 @@ from erpnext.stock.stock_ledger import (
repost_future_sle,
)
RecoverableErrors = (JobTimeoutException, QueryDeadlockError, QueryTimeoutError)
class RepostItemValuation(Document):
def validate(self):
@@ -132,7 +136,7 @@ def repost(doc):
doc.set_status("Completed")
except Exception:
except Exception as e:
frappe.db.rollback()
traceback = frappe.get_traceback()
frappe.log_error(traceback)
@@ -142,9 +146,9 @@ def repost(doc):
message += "<br>" + "Traceback: <br>" + traceback
frappe.db.set_value(doc.doctype, doc.name, "error_log", message)
notify_error_to_stock_managers(doc, message)
doc.set_status("Failed")
raise
if not isinstance(e, RecoverableErrors):
notify_error_to_stock_managers(doc, message)
doc.set_status("Failed")
finally:
if not frappe.flags.in_test:
frappe.db.commit()

View File

@@ -468,7 +468,9 @@ frappe.ui.form.on('Stock Entry', {
},
callback: function(r) {
if (!r.exc) {
$.extend(child, r.message);
["actual_qty", "basic_rate"].forEach((field) => {
frappe.model.set_value(cdt, cdn, field, (r.message[field] || 0.0));
});
frm.events.calculate_basic_amount(frm, child);
}
}
@@ -1069,8 +1071,8 @@ function attach_bom_items(bom_no) {
function check_should_not_attach_bom_items(bom_no) {
return (
bom_no === undefined ||
(erpnext.stock.bom && erpnext.stock.bom.name === bom_no)
bom_no === undefined ||
(erpnext.stock.bom && erpnext.stock.bom.name === bom_no)
);
}

View File

@@ -671,7 +671,8 @@ class StockEntry(StockController):
raise_error_if_no_rate=raise_error_if_no_rate,
)
d.basic_rate = flt(d.basic_rate, d.precision("basic_rate"))
# do not round off basic rate to avoid precision loss
d.basic_rate = flt(d.basic_rate)
if d.is_process_loss:
d.basic_rate = flt(0.0)
d.basic_amount = flt(flt(d.transfer_qty) * flt(d.basic_rate), d.precision("basic_amount"))
@@ -718,7 +719,7 @@ class StockEntry(StockController):
total_fg_qty = sum([flt(d.transfer_qty) for d in self.items if d.is_finished_item])
return flt(outgoing_items_cost / total_fg_qty)
def get_basic_rate_for_manufactured_item(self, finished_item_qty, outgoing_items_cost=0):
def get_basic_rate_for_manufactured_item(self, finished_item_qty, outgoing_items_cost=0) -> float:
scrap_items_cost = sum([flt(d.basic_amount) for d in self.get("items") if d.is_scrap_item])
# Get raw materials cost from BOM if multiple material consumption entries
@@ -758,10 +759,8 @@ class StockEntry(StockController):
for d in self.get("items"):
if d.transfer_qty:
d.amount = flt(flt(d.basic_amount) + flt(d.additional_cost), d.precision("amount"))
d.valuation_rate = flt(
flt(d.basic_rate) + (flt(d.additional_cost) / flt(d.transfer_qty)),
d.precision("valuation_rate"),
)
# Do not round off valuation rate to avoid precision loss
d.valuation_rate = flt(d.basic_rate) + (flt(d.additional_cost) / flt(d.transfer_qty))
def set_total_incoming_outgoing_value(self):
self.total_incoming_value = self.total_outgoing_value = 0.0
@@ -2196,6 +2195,12 @@ class StockEntry(StockController):
return sorted(list(set(get_serial_nos(self.pro_doc.serial_no)) - set(used_serial_nos)))
def set_missing_values(self):
"Updates rate and availability of all the items of mapped doc."
self.set_transfer_qty()
self.set_actual_qty()
self.calculate_rate_and_amount()
@frappe.whitelist()
def move_sample_to_retention_warehouse(company, items):
@@ -2245,6 +2250,7 @@ def move_sample_to_retention_warehouse(company, items):
def make_stock_in_entry(source_name, target_doc=None):
def set_missing_values(source, target):
target.set_stock_entry_type()
target.set_missing_values()
def update_item(source_doc, target_doc, source_parent):
target_doc.t_warehouse = ""

View File

@@ -133,6 +133,7 @@ def make_stock_entry(**args):
)
s.set_stock_entry_type()
if not args.do_not_save:
s.insert()
if not args.do_not_submit:

View File

@@ -1381,6 +1381,25 @@ class TestStockEntry(FrappeTestCase):
self.assertRaises(frappe.ValidationError, se.save)
def test_mapped_stock_entry(self):
"Check if rate and stock details are populated in mapped SE given warehouse."
from erpnext.stock.doctype.purchase_receipt.purchase_receipt import make_stock_entry
from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt
item_code = "_TestMappedItem"
create_item(item_code, is_stock_item=True)
pr = make_purchase_receipt(
item_code=item_code, qty=2, rate=100, company="_Test Company", warehouse="Stores - _TC"
)
mapped_se = make_stock_entry(pr.name)
self.assertEqual(mapped_se.items[0].s_warehouse, "Stores - _TC")
self.assertEqual(mapped_se.items[0].actual_qty, 2)
self.assertEqual(mapped_se.items[0].basic_rate, 100)
self.assertEqual(mapped_se.items[0].basic_amount, 200)
def make_serialized_item(**args):
args = frappe._dict(args)

View File

@@ -7,8 +7,7 @@ import frappe
from frappe.core.page.permission_manager.permission_manager import reset
from frappe.custom.doctype.property_setter.property_setter import make_property_setter
from frappe.tests.utils import FrappeTestCase, change_settings
from frappe.utils import add_days, today
from frappe.utils.data import add_to_date
from frappe.utils import add_days, add_to_date, today
from erpnext.accounts.doctype.gl_entry.gl_entry import rename_gle_sle_docs
from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note
@@ -719,6 +718,41 @@ class TestStockLedgerEntry(FrappeTestCase):
except Exception as e:
self.fail("Double processing of qty for clashing timestamp.")
@change_settings("System Settings", {"float_precision": 3, "currency_precision": 2})
def test_transfer_invariants(self):
"""Extact stock value should be transferred."""
item = make_item(
properties={
"valuation_method": "Moving Average",
"stock_uom": "Kg",
}
).name
source_warehouse = "Stores - TCP1"
target_warehouse = "Finished Goods - TCP1"
make_purchase_receipt(
item=item,
warehouse=source_warehouse,
qty=20,
conversion_factor=1000,
uom="Tonne",
rate=156_526.0,
company="_Test Company with perpetual inventory",
)
transfer = make_stock_entry(
item=item, from_warehouse=source_warehouse, to_warehouse=target_warehouse, qty=1_728.0
)
filters = {"voucher_no": transfer.name, "voucher_type": transfer.doctype, "is_cancelled": 0}
sles = frappe.get_all(
"Stock Ledger Entry",
fields=["*"],
filters=filters,
order_by="timestamp(posting_date, posting_time), creation",
)
self.assertEqual(abs(sles[0].stock_value_difference), sles[1].stock_value_difference)
def create_repack_entry(**args):
args = frappe._dict(args)

View File

@@ -0,0 +1,53 @@
// Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
/* eslint-disable */
const DIFFERNCE_FIELD_NAMES = [
"fifo_qty_diff",
"fifo_value_diff",
];
frappe.query_reports["FIFO Queue vs Qty After Transaction Comparison"] = {
"filters": [
{
"fieldname": "item_code",
"fieldtype": "Link",
"label": "Item",
"options": "Item",
get_query: function() {
return {
filters: {is_stock_item: 1, has_serial_no: 0}
}
}
},
{
"fieldname": "item_group",
"fieldtype": "Link",
"label": "Item Group",
"options": "Item Group",
},
{
"fieldname": "warehouse",
"fieldtype": "Link",
"label": "Warehouse",
"options": "Warehouse",
},
{
"fieldname": "from_date",
"fieldtype": "Date",
"label": "From Posting Date",
},
{
"fieldname": "to_date",
"fieldtype": "Date",
"label": "From Posting Date",
}
],
formatter (value, row, column, data, default_formatter) {
value = default_formatter(value, row, column, data);
if (DIFFERNCE_FIELD_NAMES.includes(column.fieldname) && Math.abs(data[column.fieldname]) > 0.001) {
value = "<span style='color:red'>" + value + "</span>";
}
return value;
},
};

View File

@@ -0,0 +1,27 @@
{
"add_total_row": 0,
"columns": [],
"creation": "2022-05-11 04:09:13.460652",
"disable_prepared_report": 0,
"disabled": 0,
"docstatus": 0,
"doctype": "Report",
"filters": [],
"idx": 0,
"is_standard": "Yes",
"letter_head": "abc",
"modified": "2022-05-11 04:09:20.232177",
"modified_by": "Administrator",
"module": "Stock",
"name": "FIFO Queue vs Qty After Transaction Comparison",
"owner": "Administrator",
"prepared_report": 0,
"ref_doctype": "Stock Ledger Entry",
"report_name": "FIFO Queue vs Qty After Transaction Comparison",
"report_type": "Script Report",
"roles": [
{
"role": "Administrator"
}
]
}

View File

@@ -0,0 +1,207 @@
# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
import json
import frappe
from frappe import _
from frappe.utils import flt
from frappe.utils.nestedset import get_descendants_of
SLE_FIELDS = (
"name",
"item_code",
"warehouse",
"posting_date",
"posting_time",
"creation",
"voucher_type",
"voucher_no",
"actual_qty",
"qty_after_transaction",
"stock_queue",
"batch_no",
"stock_value",
"valuation_rate",
)
def execute(filters=None):
columns = get_columns()
data = get_data(filters)
return columns, data
def get_data(filters):
if not any([filters.warehouse, filters.item_code, filters.item_group]):
frappe.throw(_("Any one of following filters required: warehouse, Item Code, Item Group"))
sles = get_stock_ledger_entries(filters)
return find_first_bad_queue(sles)
def get_stock_ledger_entries(filters):
sle_filters = {"is_cancelled": 0}
if filters.warehouse:
children = get_descendants_of("Warehouse", filters.warehouse)
sle_filters["warehouse"] = ("in", children + [filters.warehouse])
if filters.item_code:
sle_filters["item_code"] = filters.item_code
elif filters.get("item_group"):
item_group = filters.get("item_group")
children = get_descendants_of("Item Group", item_group)
item_group_filter = {"item_group": ("in", children + [item_group])}
sle_filters["item_code"] = (
"in",
frappe.get_all("Item", filters=item_group_filter, pluck="name", order_by=None),
)
if filters.from_date:
sle_filters["posting_date"] = (">=", filters.from_date)
if filters.to_date:
sle_filters["posting_date"] = ("<=", filters.to_date)
return frappe.get_all(
"Stock Ledger Entry",
fields=SLE_FIELDS,
filters=sle_filters,
order_by="timestamp(posting_date, posting_time), creation",
)
def find_first_bad_queue(sles):
item_warehouse_sles = {}
for sle in sles:
item_warehouse_sles.setdefault((sle.item_code, sle.warehouse), []).append(sle)
data = []
for _item_wh, sles in item_warehouse_sles.items():
for idx, sle in enumerate(sles):
queue = json.loads(sle.stock_queue or "[]")
sle.fifo_queue_qty = 0.0
sle.fifo_stock_value = 0.0
for qty, rate in queue:
sle.fifo_queue_qty += flt(qty)
sle.fifo_stock_value += flt(qty) * flt(rate)
sle.fifo_qty_diff = sle.qty_after_transaction - sle.fifo_queue_qty
sle.fifo_value_diff = sle.stock_value - sle.fifo_stock_value
if abs(sle.fifo_qty_diff) > 0.001 or abs(sle.fifo_value_diff) > 0.1:
if idx:
data.append(sles[idx - 1])
data.append(sle)
data.append({})
break
return data
def get_columns():
return [
{
"fieldname": "name",
"fieldtype": "Link",
"label": _("Stock Ledger Entry"),
"options": "Stock Ledger Entry",
},
{
"fieldname": "item_code",
"fieldtype": "Link",
"label": _("Item Code"),
"options": "Item",
},
{
"fieldname": "warehouse",
"fieldtype": "Link",
"label": _("Warehouse"),
"options": "Warehouse",
},
{
"fieldname": "posting_date",
"fieldtype": "Data",
"label": _("Posting Date"),
},
{
"fieldname": "posting_time",
"fieldtype": "Data",
"label": _("Posting Time"),
},
{
"fieldname": "creation",
"fieldtype": "Data",
"label": _("Creation"),
},
{
"fieldname": "voucher_type",
"fieldtype": "Link",
"label": _("Voucher Type"),
"options": "DocType",
},
{
"fieldname": "voucher_no",
"fieldtype": "Dynamic Link",
"label": _("Voucher No"),
"options": "voucher_type",
},
{
"fieldname": "batch_no",
"fieldtype": "Link",
"label": _("Batch"),
"options": "Batch",
},
{
"fieldname": "use_batchwise_valuation",
"fieldtype": "Check",
"label": _("Batchwise Valuation"),
},
{
"fieldname": "actual_qty",
"fieldtype": "Float",
"label": _("Qty Change"),
},
{
"fieldname": "qty_after_transaction",
"fieldtype": "Float",
"label": _("(A) Qty After Transaction"),
},
{
"fieldname": "stock_queue",
"fieldtype": "Data",
"label": _("FIFO/LIFO Queue"),
},
{
"fieldname": "fifo_queue_qty",
"fieldtype": "Float",
"label": _("(C) Total qty in queue"),
},
{
"fieldname": "fifo_qty_diff",
"fieldtype": "Float",
"label": _("A - C"),
},
{
"fieldname": "stock_value",
"fieldtype": "Float",
"label": _("(D) Balance Stock Value"),
},
{
"fieldname": "fifo_stock_value",
"fieldtype": "Float",
"label": _("(E) Balance Stock Value in Queue"),
},
{
"fieldname": "fifo_value_diff",
"fieldtype": "Float",
"label": _("D - E"),
},
{
"fieldname": "valuation_rate",
"fieldtype": "Float",
"label": _("(H) Valuation Rate"),
},
]

View File

@@ -1,6 +1,7 @@
# Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
import datetime
from typing import List
import frappe
from frappe import _, scrub
@@ -148,18 +149,26 @@ def get_periodic_data(entry, filters):
- Warehouse A : bal_qty/value
- Warehouse B : bal_qty/value
"""
expected_ranges = get_period_date_ranges(filters)
expected_periods = []
for _start_date, end_date in expected_ranges:
expected_periods.append(get_period(end_date, filters))
periodic_data = {}
for d in entry:
period = get_period(d.posting_date, filters)
bal_qty = 0
fill_intermediate_periods(periodic_data, d.item_code, period, expected_periods)
# if period against item does not exist yet, instantiate it
# insert existing balance dict against period, and add/subtract to it
if periodic_data.get(d.item_code) and not periodic_data.get(d.item_code).get(period):
previous_balance = periodic_data[d.item_code]["balance"].copy()
periodic_data[d.item_code][period] = previous_balance
if d.voucher_type == "Stock Reconciliation":
if d.voucher_type == "Stock Reconciliation" and not d.batch_no:
if periodic_data.get(d.item_code) and periodic_data.get(d.item_code).get("balance").get(
d.warehouse
):
@@ -186,6 +195,36 @@ def get_periodic_data(entry, filters):
return periodic_data
def fill_intermediate_periods(
periodic_data, item_code: str, current_period: str, all_periods: List[str]
) -> None:
"""There might be intermediate periods where no stock ledger entry exists, copy previous previous data.
Previous data is ONLY copied if period falls in report range and before period being processed currently.
args:
current_period: process till this period (exclusive)
all_periods: all periods expected in report via filters
periodic_data: report's periodic data
item_code: item_code being processed
"""
previous_period_data = None
for period in all_periods:
if period == current_period:
return
if (
periodic_data.get(item_code)
and not periodic_data.get(item_code).get(period)
and previous_period_data
):
# This period should exist since it's in report range, assign previous period data
periodic_data[item_code][period] = previous_period_data.copy()
previous_period_data = periodic_data.get(item_code, {}).get(period)
def get_data(filters):
data = []
items = get_items(filters)
@@ -194,6 +233,8 @@ def get_data(filters):
periodic_data = get_periodic_data(sle, filters)
ranges = get_period_date_ranges(filters)
today = getdate()
for dummy, item_data in item_details.items():
row = {
"name": item_data.name,
@@ -202,14 +243,15 @@ def get_data(filters):
"uom": item_data.stock_uom,
"brand": item_data.brand,
}
total = 0
for dummy, end_date in ranges:
previous_period_value = 0.0
for start_date, end_date in ranges:
period = get_period(end_date, filters)
period_data = periodic_data.get(item_data.name, {}).get(period)
amount = sum(period_data.values()) if period_data else 0
row[scrub(period)] = amount
total += amount
row["total"] = total
if period_data:
row[scrub(period)] = previous_period_value = sum(period_data.values())
else:
row[scrub(period)] = previous_period_value if today >= start_date else None
data.append(row)
return data

View File

@@ -1,13 +1,59 @@
import datetime
import frappe
from frappe import _dict
from frappe.tests.utils import FrappeTestCase
from frappe.utils.data import add_to_date, get_datetime, getdate, nowdate
from erpnext.accounts.utils import get_fiscal_year
from erpnext.stock.report.stock_analytics.stock_analytics import get_period_date_ranges
from erpnext.stock.doctype.item.test_item import make_item
from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry
from erpnext.stock.report.stock_analytics.stock_analytics import execute, get_period_date_ranges
def stock_analytics(filters):
col, data, *_ = execute(filters)
return col, data
class TestStockAnalyticsReport(FrappeTestCase):
def setUp(self) -> None:
self.item = make_item().name
self.warehouse = "_Test Warehouse - _TC"
def assert_single_item_report(self, movement, expected_buckets):
self.generate_stock(movement)
filters = _dict(
range="Monthly",
from_date=movement[0][1].replace(day=1),
to_date=movement[-1][1].replace(day=28),
value_quantity="Quantity",
company="_Test Company",
item_code=self.item,
)
cols, data = stock_analytics(filters)
self.assertEqual(len(data), 1)
row = frappe._dict(data[0])
self.assertEqual(row.name, self.item)
self.compare_analytics_row(row, cols, expected_buckets)
def generate_stock(self, movement):
for qty, posting_date in movement:
args = {"item": self.item, "qty": abs(qty), "posting_date": posting_date}
args["to_warehouse" if qty > 0 else "from_warehouse"] = self.warehouse
make_stock_entry(**args)
def compare_analytics_row(self, report_row, columns, expected_buckets):
# last (N) cols will be monthly data
no_of_buckets = len(expected_buckets)
month_cols = [col["fieldname"] for col in columns[-no_of_buckets:]]
actual_buckets = [report_row.get(col) for col in month_cols]
self.assertEqual(actual_buckets, expected_buckets)
def test_get_period_date_ranges(self):
filters = _dict(range="Monthly", from_date="2020-12-28", to_date="2021-02-06")
@@ -33,3 +79,38 @@ class TestStockAnalyticsReport(FrappeTestCase):
]
self.assertEqual(ranges, expected_ranges)
def test_basic_report_functionality(self):
"""Stock analytics report generates balance "as of" periods based on
user defined ranges. Check that this behaviour is correct."""
# create stock movement in 3 months at 15th of month
today = getdate()
movement = [
(10, add_to_date(today, months=0).replace(day=15)),
(-5, add_to_date(today, months=1).replace(day=15)),
(10, add_to_date(today, months=2).replace(day=15)),
]
self.assert_single_item_report(movement, [10, 5, 15])
def test_empty_month_in_between(self):
today = getdate()
movement = [
(100, add_to_date(today, months=0).replace(day=15)),
(-50, add_to_date(today, months=1).replace(day=15)),
# Skip a month
(20, add_to_date(today, months=3).replace(day=15)),
]
self.assert_single_item_report(movement, [100, 50, 50, 70])
def test_multi_month_missings(self):
today = getdate()
movement = [
(100, add_to_date(today, months=0).replace(day=15)),
(-50, add_to_date(today, months=1).replace(day=15)),
# Skip a month
(20, add_to_date(today, months=3).replace(day=15)),
# Skip another month
(-10, add_to_date(today, months=5).replace(day=15)),
]
self.assert_single_item_report(movement, [100, 50, 50, 70, 70, 60])

View File

@@ -52,6 +52,8 @@ REPORT_FILTER_TEST_CASES: List[Tuple[ReportName, ReportFilters]] = [
("Batch Item Expiry Status", {}),
("Stock Ageing", {"range1": 30, "range2": 60, "range3": 90, "_optional": True}),
("Stock Ledger Invariant Check", {"warehouse": "_Test Warehouse - _TC", "item": "_Test Item"}),
("FIFO Queue vs Qty After Transaction Comparison", {"warehouse": "_Test Warehouse - _TC"}),
("FIFO Queue vs Qty After Transaction Comparison", {"item_group": "All Item Groups"}),
]
OPTIONAL_FILTERS = {

View File

@@ -41,3 +41,8 @@ class TestInit(unittest.TestCase):
enc_name == expected_names[i],
"{enc} is not same as {exp}".format(enc=enc_name, exp=expected_names[i]),
)
def test_translation_files(self):
from frappe.tests.test_translate import verify_translation_files
verify_translation_files("erpnext")

View File

@@ -5055,7 +5055,7 @@ Accepted Qty,Akzeptierte Menge,
Rejected Qty,Abgelehnt Menge,
UOM Conversion Factor,Maßeinheit-Umrechnungsfaktor,
Discount on Price List Rate (%),Rabatt auf die Preisliste (%),
Price List Rate (Company Currency),Preisliste (Unternehmenswährung),
Price List Rate (Company Currency),Preisliste (Unternehmenswährung),
Rate ,Preis,
Rate (Company Currency),Preis (Unternehmenswährung),
Amount (Company Currency),Betrag (Unternehmenswährung),
@@ -6509,20 +6509,20 @@ Driver licence class,Führerscheinklasse,
HR-EMP-,HR-EMP-,
Employment Type,Art der Beschäftigung,
Emergency Contact,Notfallkontakt,
Emergency Contact Name,Notfall Kontaktname,
Emergency Phone,Notruf,
Emergency Contact Name,Name des Notfallkontakts,
Emergency Phone,Telefonnummer des Notfallkontakts,
ERPNext User,ERPNext Benutzer,
"System User (login) ID. If set, it will become default for all HR forms.","Systembenutzer-ID (Anmeldung). Wenn gesetzt, wird sie standardmäßig für alle HR-Formulare verwendet.",
Create User Permission,Benutzerberechtigung Erstellen,
This will restrict user access to other employee records,Dies schränkt den Benutzerzugriff auf andere Mitarbeiterdatensätze ein,
Joining Details,Details des Beitritts,
Offer Date,Angebotsdatum,
Confirmation Date,Datum bestätigen,
Confirmation Date,Bestätigungsdatum,
Contract End Date,Vertragsende,
Notice (days),Meldung(s)(-Tage),
Notice (days),Kündigungsfrist (Tage),
Date Of Retirement,Zeitpunkt der Pensionierung,
Department and Grade,Abteilung und Klasse,
Reports to,Berichte an,
Reports to,Vorgesetzter,
Attendance and Leave Details,Anwesenheits- und Urlaubsdetails,
Leave Policy,Urlaubsrichtlinie,
Attendance Device ID (Biometric/RF tag ID),Anwesenheitsgeräte-ID (biometrische / RF-Tag-ID),
@@ -6545,8 +6545,8 @@ Company Email,E-Mail-Adresse des Unternehmens,
Provide Email Address registered in company,Geben Sie E-Mail-Adresse in Unternehmen registriert,
Current Address Is,Aktuelle Adresse ist,
Current Address,Aktuelle Adresse,
Personal Bio,Persönliches Bio,
Bio / Cover Letter,Bio / Anschreiben,
Personal Bio,Lebenslauf,
Bio / Cover Letter,Lebenslauf / Anschreiben,
Short biography for website and other publications.,Kurzbiographie für die Webseite und andere Publikationen.,
Passport Number,Passnummer,
Date of Issue,Ausstellungsdatum,
@@ -9533,7 +9533,7 @@ Preview Email,Vorschau E-Mail,
Please select a Supplier,Bitte wählen Sie einen Lieferanten aus,
Supplier Lead Time (days),Vorlaufzeit des Lieferanten (Tage),
"Home, Work, etc.","Zuhause, Arbeit usw.",
Exit Interview Held On,Beenden Sie das Interview,
Exit Interview Held On,Entlassungsgespräch am,
Condition and formula,Zustand und Formel,
Sets 'Target Warehouse' in each row of the Items table.,Legt &#39;Ziellager&#39; in jeder Zeile der Elementtabelle fest.,
Sets 'Source Warehouse' in each row of the Items table.,Legt &#39;Source Warehouse&#39; in jeder Zeile der Items-Tabelle fest.,
Can't render this file because it is too large.

File diff suppressed because it is too large Load Diff