Merge branch 'version-13-hotfix' of https://github.com/frappe/erpnext into quotation_gst

This commit is contained in:
Deepesh Garg
2022-03-27 17:28:42 +05:30
42 changed files with 414 additions and 142 deletions

View File

@@ -1761,6 +1761,7 @@ def make_delivery_note(source_name, target_doc=None):
}
}, target_doc, set_missing_values)
doclist.set_onload('ignore_price_list', True)
return doclist
@frappe.whitelist()

View File

@@ -812,12 +812,37 @@ class TestSalesInvoice(unittest.TestCase):
pos.append("payments", {'mode_of_payment': 'Bank Draft', 'account': '_Test Bank - TCP1', 'amount': 50})
pos.append("payments", {'mode_of_payment': 'Cash', 'account': 'Cash - TCP1', 'amount': 60})
pos.change_amount = 5.0
pos.write_off_outstanding_amount_automatically = 1
pos.insert()
pos.submit()
self.assertEqual(pos.grand_total, 100.0)
self.assertEqual(pos.write_off_amount, -5)
self.assertEqual(pos.write_off_amount, 0)
def test_auto_write_off_amount(self):
make_pos_profile(company="_Test Company with perpetual inventory", income_account = "Sales - TCP1",
expense_account = "Cost of Goods Sold - TCP1", warehouse="Stores - TCP1", cost_center = "Main - TCP1", write_off_account="_Test Write Off - TCP1")
make_purchase_receipt(company= "_Test Company with perpetual inventory",
item_code= "_Test FG Item",warehouse= "Stores - TCP1", cost_center= "Main - TCP1")
pos = create_sales_invoice(company= "_Test Company with perpetual inventory",
debit_to="Debtors - TCP1", item_code= "_Test FG Item", warehouse="Stores - TCP1",
income_account = "Sales - TCP1", expense_account = "Cost of Goods Sold - TCP1",
cost_center = "Main - TCP1", do_not_save=True)
pos.is_pos = 1
pos.update_stock = 1
pos.append("payments", {'mode_of_payment': 'Bank Draft', 'account': '_Test Bank - TCP1', 'amount': 50})
pos.append("payments", {'mode_of_payment': 'Cash', 'account': 'Cash - TCP1', 'amount': 40})
pos.write_off_outstanding_amount_automatically = 1
pos.insert()
pos.submit()
self.assertEqual(pos.grand_total, 100.0)
self.assertEqual(pos.write_off_amount, 10)
def test_pos_with_no_gl_entry_for_change_amount(self):
frappe.db.set_value('Accounts Settings', None, 'post_change_gl_entries', 0)

View File

@@ -5,7 +5,7 @@
import frappe
from frappe import _
from frappe.query_builder.functions import Sum
from frappe.utils import add_to_date, get_date_str
from frappe.utils import add_to_date, flt, get_date_str
from erpnext.accounts.report.financial_statements import get_columns, get_data, get_period_list
from erpnext.accounts.report.profit_and_loss_statement.profit_and_loss_statement import (
@@ -442,8 +442,8 @@ def _get_account_type_based_data(filters, account_names, period_list, accumulate
else:
gl_sum = 0
total += gl_sum
data.setdefault(period["key"], gl_sum)
total += flt(gl_sum)
data.setdefault(period["key"], flt(gl_sum))
data["total"] = total
return data

View File

@@ -442,6 +442,8 @@ def make_purchase_receipt(source_name, target_doc=None):
}
}, target_doc, set_missing_values)
doc.set_onload('ignore_price_list', True)
return doc
@frappe.whitelist()
@@ -509,6 +511,7 @@ def get_mapped_purchase_invoice(source_name, target_doc=None, ignore_permissions
doc = get_mapped_doc("Purchase Order", source_name, fields,
target_doc, postprocess, ignore_permissions=ignore_permissions)
doc.set_onload('ignore_price_list', True)
return doc

View File

@@ -139,6 +139,7 @@ def make_purchase_order(source_name, target_doc=None):
},
}, target_doc, set_missing_values)
doclist.set_onload('ignore_price_list', True)
return doclist
@frappe.whitelist()

View File

@@ -168,7 +168,7 @@ def tax_account_query(doctype, txt, searchfield, start, page_len, filters):
{account_type_condition}
AND is_group = 0
AND company = %(company)s
AND account_currency = %(currency)s
AND (account_currency = %(currency)s or ifnull(account_currency, '') = '')
AND `{searchfield}` LIKE %(txt)s
{mcond}
ORDER BY idx DESC, name

View File

@@ -399,6 +399,8 @@ def make_return_doc(doctype, source_name, target_doc=None):
}
}, target_doc, set_missing_values)
doclist.set_onload('ignore_price_list', True)
return doclist
def get_rate_for_return(voucher_type, voucher_no, item_code, return_against=None,

View File

@@ -114,20 +114,16 @@ class calculate_taxes_and_totals(object):
for item in self.doc.get("items"):
self.doc.round_floats_in(item)
if not item.rate:
item.rate = item.price_list_rate
if item.discount_percentage == 100:
item.rate = 0.0
elif item.price_list_rate:
if item.pricing_rules or abs(item.discount_percentage) > 0:
if not item.rate or (item.pricing_rules and item.discount_percentage > 0):
item.rate = flt(item.price_list_rate *
(1.0 - (item.discount_percentage / 100.0)), item.precision("rate"))
if abs(item.discount_percentage) > 0:
item.discount_amount = item.price_list_rate * (item.discount_percentage / 100.0)
item.discount_amount = item.price_list_rate * (item.discount_percentage / 100.0)
elif item.discount_amount or item.pricing_rules:
elif item.discount_amount and item.pricing_rules:
item.rate = item.price_list_rate - item.discount_amount
if item.doctype in ['Quotation Item', 'Sales Order Item', 'Delivery Note Item', 'Sales Invoice Item',
@@ -584,7 +580,11 @@ class calculate_taxes_and_totals(object):
.format(self.doc.party_account_currency, invoice_total))
if self.doc.docstatus == 0:
if self.doc.get('write_off_outstanding_amount_automatically'):
self.doc.write_off_amount = 0
self.calculate_outstanding_amount()
self.calculate_write_off_amount()
def is_internal_invoice(self):
"""
@@ -625,7 +625,6 @@ class calculate_taxes_and_totals(object):
change_amount = 0
if self.doc.doctype == "Sales Invoice" and not self.doc.get('is_return'):
self.calculate_write_off_amount()
self.calculate_change_amount()
change_amount = self.doc.change_amount \
if self.doc.party_account_currency == self.doc.currency else self.doc.base_change_amount
@@ -675,19 +674,20 @@ class calculate_taxes_and_totals(object):
and self.doc.paid_amount > grand_total and not self.doc.is_return \
and any(d.type == "Cash" for d in self.doc.payments):
self.doc.change_amount = flt(self.doc.paid_amount - grand_total +
self.doc.write_off_amount, self.doc.precision("change_amount"))
self.doc.change_amount = flt(self.doc.paid_amount - grand_total,
self.doc.precision("change_amount"))
self.doc.base_change_amount = flt(self.doc.base_paid_amount - base_grand_total +
self.doc.base_write_off_amount, self.doc.precision("base_change_amount"))
self.doc.base_change_amount = flt(self.doc.base_paid_amount - base_grand_total,
self.doc.precision("base_change_amount"))
def calculate_write_off_amount(self):
if flt(self.doc.change_amount) > 0:
self.doc.write_off_amount = flt(self.doc.grand_total - self.doc.paid_amount
+ self.doc.change_amount, self.doc.precision("write_off_amount"))
if self.doc.get('write_off_outstanding_amount_automatically'):
self.doc.write_off_amount = flt(self.doc.outstanding_amount, self.doc.precision("write_off_amount"))
self.doc.base_write_off_amount = flt(self.doc.write_off_amount * self.doc.conversion_rate,
self.doc.precision("base_write_off_amount"))
self.calculate_outstanding_amount()
def calculate_margin(self, item):
rate_with_margin = 0.0
base_rate_with_margin = 0.0

View File

@@ -5,6 +5,12 @@ frappe.ui.form.on('Website Item', {
onload: function(frm) {
// should never check Private
frm.fields_dict["website_image"].df.is_private = 0;
frm.set_query("website_warehouse", () => {
return {
filters: {"is_group": 0}
};
});
},
image: function() {

View File

@@ -424,6 +424,22 @@ erpnext.ProductView = class {
me.change_route_with_filters();
});
// bind filter lookup input box
$('.filter-lookup-input').on('keydown', frappe.utils.debounce((e) => {
const $input = $(e.target);
const keyword = ($input.val() || '').toLowerCase();
const $filter_options = $input.next('.filter-options');
$filter_options.find('.filter-lookup-wrapper').show();
$filter_options.find('.filter-lookup-wrapper').each((i, el) => {
const $el = $(el);
const value = $el.data('value').toLowerCase();
if (!value.includes(keyword)) {
$el.hide();
}
});
}, 300));
}
change_route_with_filters() {

View File

@@ -4,7 +4,7 @@
import unittest
import frappe
from frappe.utils import nowdate
from frappe.utils import flt, nowdate
import erpnext
from erpnext.hr.doctype.employee.test_employee import make_employee
@@ -13,6 +13,7 @@ from erpnext.hr.doctype.employee_advance.employee_advance import (
create_return_through_additional_salary,
make_bank_entry,
)
from erpnext.hr.doctype.expense_claim.expense_claim import get_advances
from erpnext.payroll.doctype.salary_component.test_salary_component import create_salary_component
from erpnext.payroll.doctype.salary_structure.test_salary_structure import make_salary_structure
@@ -118,3 +119,24 @@ def make_employee_advance(employee_name, args=None):
doc.submit()
return doc
def get_advances_for_claim(claim, advance_name, amount=None):
advances = get_advances(claim.employee, advance_name)
for entry in advances:
if amount:
allocated_amount = amount
else:
allocated_amount = flt(entry.paid_amount) - flt(entry.claimed_amount)
claim.append("advances", {
"employee_advance": entry.name,
"posting_date": entry.posting_date,
"advance_account": entry.advance_account,
"advance_paid": entry.paid_amount,
"unclaimed_amount": allocated_amount,
"allocated_amount": allocated_amount
})
return claim

View File

@@ -23,10 +23,10 @@ class ExpenseClaim(AccountsController):
def validate(self):
validate_active_employee(self.employee)
self.validate_advances()
set_employee_name(self)
self.validate_sanctioned_amount()
self.calculate_total_amount()
set_employee_name(self)
self.validate_advances()
self.set_expense_account(validate=True)
self.set_payable_account()
self.set_cost_center()
@@ -42,10 +42,18 @@ class ExpenseClaim(AccountsController):
"2": "Cancelled"
}[cstr(self.docstatus or 0)]
paid_amount = flt(self.total_amount_reimbursed) + flt(self.total_advance_amount)
precision = self.precision("grand_total")
if (self.is_paid or (flt(self.total_sanctioned_amount) > 0 and self.docstatus == 1
and flt(self.grand_total, precision) == flt(paid_amount, precision))) and self.approval_status == 'Approved':
if (
# set as paid
self.is_paid
or (flt(self.total_sanctioned_amount > 0) and (
# grand total is reimbursed
(self.docstatus == 1 and flt(self.grand_total, precision) == flt(self.total_amount_reimbursed, precision))
# grand total (to be paid) is 0 since linked advances already cover the claimed amount
or (flt(self.grand_total, precision) == 0)
))
) and self.approval_status == "Approved":
status = "Paid"
elif flt(self.total_sanctioned_amount) > 0 and self.docstatus == 1 and self.approval_status == 'Approved':
status = "Unpaid"

View File

@@ -72,6 +72,72 @@ class TestExpenseClaim(unittest.TestCase):
expense_claim = frappe.get_doc("Expense Claim", expense_claim.name)
self.assertEqual(expense_claim.status, "Unpaid")
# expense claim without any sanctioned amount should not have status as Paid
claim = make_expense_claim(payable_account, 1000, 0, "_Test Company", "Travel Expenses - _TC")
self.assertEqual(claim.total_sanctioned_amount, 0)
self.assertEqual(claim.status, "Submitted")
# no gl entries created
gl_entry = frappe.get_all('GL Entry', {'voucher_type': 'Expense Claim', 'voucher_no': claim.name})
self.assertEqual(len(gl_entry), 0)
def test_expense_claim_against_fully_paid_advances(self):
from erpnext.hr.doctype.employee_advance.test_employee_advance import (
get_advances_for_claim,
make_employee_advance,
make_payment_entry,
)
frappe.db.delete("Employee Advance")
payable_account = get_payable_account("_Test Company")
claim = make_expense_claim(payable_account, 1000, 1000, "_Test Company", "Travel Expenses - _TC", do_not_submit=True)
advance = make_employee_advance(claim.employee)
pe = make_payment_entry(advance)
pe.submit()
# claim for already paid out advances
claim = get_advances_for_claim(claim, advance.name)
claim.save()
claim.submit()
self.assertEqual(claim.grand_total, 0)
self.assertEqual(claim.status, "Paid")
def test_expense_claim_partially_paid_via_advance(self):
from erpnext.hr.doctype.employee_advance.test_employee_advance import (
get_advances_for_claim,
make_employee_advance,
)
from erpnext.hr.doctype.employee_advance.test_employee_advance import (
make_payment_entry as make_advance_payment,
)
frappe.db.delete("Employee Advance")
payable_account = get_payable_account("_Test Company")
claim = make_expense_claim(payable_account, 1000, 1000, "_Test Company", "Travel Expenses - _TC", do_not_submit=True)
# link advance for partial amount
advance = make_employee_advance(claim.employee, {'advance_amount': 500})
pe = make_advance_payment(advance)
pe.submit()
claim = get_advances_for_claim(claim, advance.name)
claim.save()
claim.submit()
self.assertEqual(claim.grand_total, 500)
self.assertEqual(claim.status, "Unpaid")
# reimburse remaning amount
make_payment_entry(claim, payable_account, 500)
claim.reload()
self.assertEqual(claim.total_amount_reimbursed, 500)
self.assertEqual(claim.status, "Paid")
def test_expense_claim_gl_entry(self):
payable_account = get_payable_account(company_name)
taxes = generate_taxes()

View File

@@ -25,6 +25,7 @@ from erpnext.hr.doctype.leave_application.leave_application import (
LeaveDayBlockedError,
NotAnOptionalHoliday,
OverlapError,
get_leave_allocation_records,
get_leave_balance_on,
get_leave_details,
)
@@ -881,6 +882,27 @@ class TestLeaveApplication(unittest.TestCase):
self.assertEqual(leave_allocation['leaves_pending_approval'], 1)
self.assertEqual(leave_allocation['remaining_leaves'], 26)
@set_holiday_list('Salary Slip Test Holiday List', '_Test Company')
def test_get_leave_allocation_records(self):
employee = get_employee()
leave_type = create_leave_type(
leave_type_name="_Test_CF_leave_expiry",
is_carry_forward=1,
expire_carry_forwarded_leaves_after_days=90)
leave_type.insert()
leave_alloc = create_carry_forwarded_allocation(employee, leave_type)
details = get_leave_allocation_records(employee.name, getdate(), leave_type.name)
expected_data = {
"from_date": getdate(leave_alloc.from_date),
"to_date": getdate(leave_alloc.to_date),
"total_leaves_allocated": 30.0,
"unused_leaves": 15.0,
"new_leaves_allocated": 15.0,
"leave_type": leave_type.name
}
self.assertEqual(details.get(leave_type.name), expected_data)
def create_carry_forwarded_allocation(employee, leave_type):
# initial leave allocation
@@ -902,6 +924,8 @@ def create_carry_forwarded_allocation(employee, leave_type):
carry_forward=1)
leave_allocation.submit()
return leave_allocation
def make_allocation_record(employee=None, leave_type=None, from_date=None, to_date=None, carry_forward=False, leaves=None):
allocation = frappe.get_doc({
"doctype": "Leave Allocation",

View File

@@ -500,6 +500,9 @@ class JobCard(Document):
2: "Cancelled"
}[self.docstatus or 0]
if self.for_quantity <= self.transferred_qty:
self.status = 'Material Transferred'
if self.time_logs:
self.status = 'Work In Progress'
@@ -507,10 +510,6 @@ class JobCard(Document):
(self.for_quantity <= self.total_completed_qty or not self.items)):
self.status = 'Completed'
if self.status != 'Completed':
if self.for_quantity <= self.transferred_qty:
self.status = 'Material Transferred'
if update_status:
self.db_set('status', self.status)

View File

@@ -169,6 +169,7 @@ class TestJobCard(FrappeTestCase):
job_card_name = frappe.db.get_value("Job Card", {'work_order': self.work_order.name})
job_card = frappe.get_doc("Job Card", job_card_name)
self.assertEqual(job_card.status, "Open")
# fully transfer both RMs
transfer_entry_1 = make_stock_entry_from_jc(job_card_name)

View File

@@ -2,6 +2,13 @@
// For license information, please see license.txt
frappe.ui.form.on('Production Plan', {
before_save: function(frm) {
// preserve temporary names on production plan item to re-link sub-assembly items
frm.doc.po_items.forEach(item => {
item.temporary_name = item.name;
});
},
setup: function(frm) {
frm.custom_make_buttons = {
'Work Order': 'Work Order / Subcontract PO',

View File

@@ -351,7 +351,6 @@
"hide_border": 1
},
{
"depends_on": "get_items_from",
"fieldname": "sub_assembly_items",
"fieldtype": "Table",
"label": "Sub Assembly Items",
@@ -379,7 +378,7 @@
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
"modified": "2022-03-14 03:56:23.209247",
"modified": "2022-03-25 09:15:25.017664",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "Production Plan",

View File

@@ -32,6 +32,7 @@ class ProductionPlan(Document):
self.set_pending_qty_in_row_without_reference()
self.calculate_total_planned_qty()
self.set_status()
self._rename_temporary_references()
def set_pending_qty_in_row_without_reference(self):
"Set Pending Qty in independent rows (not from SO or MR)."
@@ -57,6 +58,18 @@ class ProductionPlan(Document):
if not flt(d.planned_qty):
frappe.throw(_("Please enter Planned Qty for Item {0} at row {1}").format(d.item_code, d.idx))
def _rename_temporary_references(self):
""" po_items and sub_assembly_items items are both constructed client side without saving.
Attempt to fix linkages by using temporary names to map final row names.
"""
new_name_map = {d.temporary_name: d.name for d in self.po_items if d.temporary_name}
actual_names = {d.name for d in self.po_items}
for sub_assy in self.sub_assembly_items:
if sub_assy.production_plan_item not in actual_names:
sub_assy.production_plan_item = new_name_map.get(sub_assy.production_plan_item)
@frappe.whitelist()
def get_open_sales_orders(self):
""" Pull sales orders which are pending to deliver based on criteria selected"""

View File

@@ -617,6 +617,39 @@ class TestProductionPlan(FrappeTestCase):
wo_doc.submit()
self.assertEqual(wo_doc.qty, 0.55)
def test_temporary_name_relinking(self):
pp = frappe.new_doc("Production Plan")
# this can not be unittested so mocking data that would be expected
# from client side.
for _ in range(10):
po_item = pp.append("po_items", {
"name": frappe.generate_hash(length=10),
"temporary_name": frappe.generate_hash(length=10),
})
pp.append("sub_assembly_items", {
"production_plan_item": po_item.temporary_name
})
pp._rename_temporary_references()
for po_item, subassy_item in zip(pp.po_items, pp.sub_assembly_items):
self.assertEqual(po_item.name, subassy_item.production_plan_item)
# bad links should be erased
pp.append("sub_assembly_items", {
"production_plan_item": frappe.generate_hash(length=16)
})
pp._rename_temporary_references()
self.assertIsNone(pp.sub_assembly_items[-1].production_plan_item)
pp.sub_assembly_items.pop()
# reattempting on same doc shouldn't change anything
pp._rename_temporary_references()
for po_item, subassy_item in zip(pp.po_items, pp.sub_assembly_items):
self.assertEqual(po_item.name, subassy_item.production_plan_item)
def create_production_plan(**args):
"""
sales_order (obj): Sales Order Doc Object

View File

@@ -27,7 +27,8 @@
"material_request",
"material_request_item",
"product_bundle_item",
"item_reference"
"item_reference",
"temporary_name"
],
"fields": [
{
@@ -204,17 +205,25 @@
"fieldtype": "Data",
"hidden": 1,
"label": "Item Reference"
},
{
"fieldname": "temporary_name",
"fieldtype": "Data",
"hidden": 1,
"label": "temporary name"
}
],
"idx": 1,
"istable": 1,
"links": [],
"modified": "2021-06-28 18:31:06.822168",
"modified": "2022-03-24 04:54:09.940224",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "Production Plan Item",
"naming_rule": "Random",
"owner": "Administrator",
"permissions": [],
"sort_field": "modified",
"sort_order": "ASC"
"sort_order": "ASC",
"states": []
}

View File

@@ -352,7 +352,6 @@ class TestWorkOrder(FrappeTestCase):
wo_order = make_wo_order_test_record(planned_start_date=now(),
sales_order=so.name, qty=3)
wo_order.submit()
self.assertEqual(wo_order.docstatus, 1)
allow_overproduction("overproduction_percentage_for_sales_order", 0)

View File

@@ -457,7 +457,8 @@ class WorkOrder(Document):
mr_obj.update_requested_qty([self.material_request_item])
def update_ordered_qty(self):
if self.production_plan and self.production_plan_item:
if self.production_plan and self.production_plan_item \
and not self.production_plan_sub_assembly_item:
qty = frappe.get_value("Production Plan Item", self.production_plan_item, "ordered_qty") or 0.0
if self.docstatus == 1:
@@ -640,9 +641,13 @@ class WorkOrder(Document):
if not self.qty > 0:
frappe.throw(_("Quantity to Manufacture must be greater than 0."))
if self.production_plan and self.production_plan_item:
if self.production_plan and self.production_plan_item \
and not self.production_plan_sub_assembly_item:
qty_dict = frappe.db.get_value("Production Plan Item", self.production_plan_item, ["planned_qty", "ordered_qty"], as_dict=1)
if not qty_dict:
return
allowance_qty = flt(frappe.db.get_single_value("Manufacturing Settings",
"overproduction_percentage_for_work_order"))/100 * qty_dict.get("planned_qty", 0)
@@ -1146,6 +1151,10 @@ def create_job_card(work_order, row, enable_capacity_planning=False, auto_create
doc.insert()
frappe.msgprint(_("Job card {0} created").format(get_link_to_form("Job Card", doc.name)), alert=True)
if enable_capacity_planning:
# automatically added scheduling rows shouldn't change status to WIP
doc.db_set("status", "Open")
return doc
def get_work_order_operation_data(work_order, operation, workstation):

View File

@@ -1,6 +1,6 @@
erpnext.patches.v12_0.update_is_cancelled_field
erpnext.patches.v13_0.add_bin_unique_constraint
erpnext.patches.v11_0.rename_production_order_to_work_order
erpnext.patches.v13_0.add_bin_unique_constraint
erpnext.patches.v11_0.refactor_naming_series
erpnext.patches.v11_0.refactor_autoname_naming
execute:frappe.reload_doc("accounts", "doctype", "POS Payment Method") #2020-05-28
@@ -352,7 +352,8 @@ erpnext.patches.v13_0.update_reserved_qty_closed_wo
erpnext.patches.v13_0.amazon_mws_deprecation_warning
erpnext.patches.v13_0.set_work_order_qty_in_so_from_mr
erpnext.patches.v13_0.update_accounts_in_loan_docs
erpnext.patches.v13_0.remove_unknown_links_to_prod_plan_items
erpnext.patches.v13_0.remove_unknown_links_to_prod_plan_items # 24-03-2022
erpnext.patches.v13_0.rename_non_profit_fields
erpnext.patches.v13_0.enable_ksa_vat_docs #1
erpnext.patches.v13_0.create_gst_custom_fields_in_quotation
erpnext.patches.v13_0.create_gst_custom_fields_in_quotation
erpnext.patches.v13_0.update_expense_claim_status_for_paid_advances

View File

@@ -60,7 +60,7 @@ def execute():
def convert_to_seconds(value, unit):
seconds = 0
if value == 0:
if not value:
return seconds
if unit == 'Hours':
seconds = value * 3600

View File

@@ -0,0 +1,22 @@
import frappe
def execute():
"""
Update Expense Claim status to Paid if:
- the entire required amount is already covered via linked advances
- the claim is partially paid via advances and the rest is reimbursed
"""
ExpenseClaim = frappe.qb.DocType('Expense Claim')
(frappe.qb
.update(ExpenseClaim)
.set(ExpenseClaim.status, 'Paid')
.where(
((ExpenseClaim.grand_total == 0) | (ExpenseClaim.grand_total == ExpenseClaim.total_amount_reimbursed))
& (ExpenseClaim.approval_status == 'Approved')
& (ExpenseClaim.docstatus == 1)
& (ExpenseClaim.total_sanctioned_amount > 0)
)
).run()

View File

@@ -660,6 +660,8 @@ def submit_salary_slips_for_employees(payroll_entry, salary_slips, publish_progr
if not_submitted_ss:
frappe.msgprint(_("Could not submit some Salary Slips"))
frappe.flags.via_payroll_entry = False
@frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs
def get_payroll_entries_for_jv(doctype, txt, searchfield, start, page_len, filters):

View File

@@ -38,6 +38,8 @@ from erpnext.payroll.doctype.salary_structure.salary_structure import make_salar
class TestSalarySlip(unittest.TestCase):
def setUp(self):
setup_test()
frappe.flags.pop("via_payroll_entry", None)
def tearDown(self):
frappe.db.rollback()
@@ -413,15 +415,17 @@ class TestSalarySlip(unittest.TestCase):
"email_salary_slip_to_employee": 1
})
def test_email_salary_slip(self):
frappe.db.sql("delete from `tabEmail Queue`")
frappe.db.delete("Email Queue")
make_employee("test_email_salary_slip@salary.com", company="_Test Company")
ss = make_employee_salary_slip("test_email_salary_slip@salary.com", "Monthly", "Test Salary Slip Email")
user_id = "test_email_salary_slip@salary.com"
make_employee(user_id, company="_Test Company")
ss = make_employee_salary_slip(user_id, "Monthly", "Test Salary Slip Email")
ss.company = "_Test Company"
ss.save()
ss.submit()
email_queue = frappe.db.sql("""select name from `tabEmail Queue`""")
email_queue = frappe.db.a_row_exists("Email Queue")
self.assertTrue(email_queue)
def test_loan_repayment_salary_slip(self):

View File

@@ -278,26 +278,6 @@ erpnext.taxes_and_totals = erpnext.payments.extend({
}
},
add_taxes_from_item_tax_template: function(item_tax_map) {
let me = this;
if (item_tax_map && cint(frappe.defaults.get_default("add_taxes_from_item_tax_template"))) {
if (typeof (item_tax_map) == "string") {
item_tax_map = JSON.parse(item_tax_map);
}
$.each(item_tax_map, function(tax, rate) {
let found = (me.frm.doc.taxes || []).find(d => d.account_head === tax);
if (!found) {
let child = frappe.model.add_child(me.frm.doc, "taxes");
child.charge_type = "On Net Total";
child.account_head = tax;
child.rate = 0;
}
});
}
},
calculate_taxes: function() {
var me = this;
this.frm.doc.rounding_adjustment = 0;
@@ -687,7 +667,12 @@ erpnext.taxes_and_totals = erpnext.payments.extend({
}));
this.frm.doc.total_advance = flt(total_allocated_amount, precision("total_advance"));
if (this.frm.doc.write_off_outstanding_amount_automatically) {
this.frm.doc.write_off_amount = 0;
}
this.calculate_outstanding_amount(update_paid_amount);
this.calculate_write_off_amount();
},
is_internal_invoice: function() {
@@ -812,7 +797,7 @@ erpnext.taxes_and_totals = erpnext.payments.extend({
this.frm.set_value('base_paid_amount', flt(base_paid_amount, precision("base_paid_amount")));
},
calculate_change_amount: function(){
calculate_change_amount: function() {
this.frm.doc.change_amount = 0.0;
this.frm.doc.base_change_amount = 0.0;
if(in_list(["Sales Invoice", "POS Invoice"], this.frm.doc.doctype)
@@ -823,26 +808,23 @@ erpnext.taxes_and_totals = erpnext.payments.extend({
var grand_total = this.frm.doc.rounded_total || this.frm.doc.grand_total;
var base_grand_total = this.frm.doc.base_rounded_total || this.frm.doc.base_grand_total;
this.frm.doc.change_amount = flt(this.frm.doc.paid_amount - grand_total +
this.frm.doc.write_off_amount, precision("change_amount"));
this.frm.doc.change_amount = flt(this.frm.doc.paid_amount - grand_total,
precision("change_amount"));
this.frm.doc.base_change_amount = flt(this.frm.doc.base_paid_amount -
base_grand_total + this.frm.doc.base_write_off_amount,
precision("base_change_amount"));
base_grand_total, precision("base_change_amount"));
}
}
},
calculate_write_off_amount: function(){
if(this.frm.doc.paid_amount > this.frm.doc.grand_total){
this.frm.doc.write_off_amount = flt(this.frm.doc.grand_total - this.frm.doc.paid_amount
+ this.frm.doc.change_amount, precision("write_off_amount"));
calculate_write_off_amount: function() {
if (this.frm.doc.write_off_outstanding_amount_automatically) {
this.frm.doc.write_off_amount = flt(this.frm.doc.outstanding_amount, precision("write_off_amount"));
this.frm.doc.base_write_off_amount = flt(this.frm.doc.write_off_amount * this.frm.doc.conversion_rate,
precision("base_write_off_amount"));
}else{
this.frm.doc.paid_amount = 0.0;
this.calculate_outstanding_amount(false);
}
this.calculate_outstanding_amount(false);
}
});

View File

@@ -736,6 +736,26 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({
});
},
add_taxes_from_item_tax_template: function(item_tax_map) {
let me = this;
if (item_tax_map && cint(frappe.defaults.get_default("add_taxes_from_item_tax_template"))) {
if (typeof (item_tax_map) == "string") {
item_tax_map = JSON.parse(item_tax_map);
}
$.each(item_tax_map, function(tax, rate) {
let found = (me.frm.doc.taxes || []).find(d => d.account_head === tax);
if (!found) {
let child = frappe.model.add_child(me.frm.doc, "taxes");
child.charge_type = "On Net Total";
child.account_head = tax;
child.rate = 0;
}
});
}
},
serial_no: function(doc, cdt, cdn) {
var me = this;
var item = frappe.get_doc(cdt, cdn);
@@ -1021,9 +1041,9 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({
var me = this;
this.set_dynamic_labels();
var company_currency = this.get_company_currency();
// Added `ignore_pricing_rule` to determine if document is loading after mapping from another doc
// Added `ignore_price_list` to determine if document is loading after mapping from another doc
if(this.frm.doc.currency && this.frm.doc.currency !== company_currency
&& !this.frm.doc.ignore_pricing_rule) {
&& !(this.frm.doc.__onload && this.frm.doc.__onload.ignore_price_list)) {
this.get_exchange_rate(transaction_date, this.frm.doc.currency, company_currency,
function(exchange_rate) {
@@ -1049,7 +1069,7 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({
}
if(flt(this.frm.doc.conversion_rate)>0.0) {
if(this.frm.doc.ignore_pricing_rule) {
if(this.frm.doc.__onload && this.frm.doc.__onload.ignore_price_list) {
this.calculate_taxes_and_totals();
} else if (!this.in_apply_price_list){
this.apply_price_list();
@@ -1123,8 +1143,9 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({
this.set_dynamic_labels();
var company_currency = this.get_company_currency();
// Added `ignore_pricing_rule` to determine if document is loading after mapping from another doc
if(this.frm.doc.price_list_currency !== company_currency && !this.frm.doc.ignore_pricing_rule) {
// Added `ignore_price_list` to determine if document is loading after mapping from another doc
if(this.frm.doc.price_list_currency !== company_currency &&
!(this.frm.doc.__onload && this.frm.doc.__onload.ignore_price_list)) {
this.get_exchange_rate(this.frm.doc.posting_date, this.frm.doc.price_list_currency, company_currency,
function(exchange_rate) {
me.frm.set_value("plc_conversion_rate", exchange_rate);
@@ -1863,6 +1884,7 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({
callback: function(r) {
if(!r.exc) {
item.item_tax_rate = r.message;
me.add_taxes_from_item_tax_template(item.item_tax_rate);
me.calculate_taxes_and_totals();
}
}

View File

@@ -606,6 +606,11 @@ function check_can_calculate_pending_qty(me) {
&& doc.fg_completed_qty
&& erpnext.stock.bom
&& erpnext.stock.bom.name === doc.bom_no;
const itemChecks = !!item && !item.allow_alternative_item;
const itemChecks = !!item
&& !item.allow_alternative_item
&& erpnext.stock.bom && erpnext.stock.items
&& (item.item_code in erpnext.stock.bom.items);
return docChecks && itemChecks;
}
//# sourceURL=serial_no_batch_selector.js

View File

@@ -4,6 +4,7 @@
--green-info: #38A160;
--product-bg-color: white;
--body-bg-color: var(--gray-50);
--text-md: 13px; // variables are in desk folder in frappe for v13, this is a temporary fix
}
body.product-page {
@@ -264,6 +265,15 @@ body.product-page {
font-size: 13px;
}
.filter-lookup-input {
background-color: white;
border: 1px solid var(--gray-300);
&:focus {
border: 1px solid var(--primary);
}
}
.filter-label {
font-size: 11px;
font-weight: 600;

View File

@@ -21,8 +21,9 @@ PAN_NUMBER_FORMAT = re.compile("[A-Z]{5}[0-9]{4}[A-Z]{1}")
def validate_gstin_for_india(doc, method):
if hasattr(doc, 'gst_state') and doc.gst_state:
doc.gst_state_number = state_numbers[doc.gst_state]
if hasattr(doc, 'gst_state'):
set_gst_state_and_state_number(doc)
if not hasattr(doc, 'gstin') or not doc.gstin:
return
@@ -52,7 +53,6 @@ def validate_gstin_for_india(doc, method):
frappe.throw(_("The input you've entered doesn't match the format of GSTIN."), title=_("Invalid GSTIN"))
validate_gstin_check_digit(doc.gstin)
set_gst_state_and_state_number(doc)
if not doc.gst_state:
frappe.throw(_("Please enter GST state"), title=_("Invalid State"))
@@ -84,17 +84,14 @@ def update_gst_category(doc, method):
frappe.db.set_value(link.link_doctype, {'name': link.link_name, 'gst_category': 'Unregistered'}, 'gst_category', 'Registered Regular')
def set_gst_state_and_state_number(doc):
if not doc.gst_state:
if not doc.state:
return
if not doc.gst_state and doc.state:
state = doc.state.lower()
states_lowercase = {s.lower():s for s in states}
if state in states_lowercase:
doc.gst_state = states_lowercase[state]
else:
return
doc.gst_state_number = state_numbers[doc.gst_state]
doc.gst_state_number = state_numbers.get(doc.gst_state)
def validate_gstin_check_digit(gstin, label='GSTIN'):
''' Function to validate the check digit of the GSTIN.'''

View File

@@ -192,6 +192,7 @@ def _make_sales_order(source_name, target_doc=None, ignore_permissions=False):
}, target_doc, set_missing_values, ignore_permissions=ignore_permissions)
# postprocess: fetch shipping address, set missing values
doclist.set_onload('ignore_price_list', True)
return doclist
@@ -255,6 +256,8 @@ def _make_sales_invoice(source_name, target_doc=None, ignore_permissions=False):
}
}, target_doc, set_missing_values, ignore_permissions=ignore_permissions)
doclist.set_onload('ignore_price_list', True)
return doclist
def _make_customer(source_name, ignore_permissions=False):

View File

@@ -630,6 +630,8 @@ def make_delivery_note(source_name, target_doc=None, skip_item_mapping=False):
target_doc = get_mapped_doc("Sales Order", source_name, mapper, target_doc, set_missing_values)
target_doc.set_onload('ignore_price_list', True)
return target_doc
@frappe.whitelist()
@@ -710,6 +712,8 @@ def make_sales_invoice(source_name, target_doc=None, ignore_permissions=False):
if automatically_fetch_payment_terms:
doclist.set_payment_schedule()
doclist.set_onload('ignore_price_list', True)
return doclist
@frappe.whitelist()

View File

@@ -519,6 +519,8 @@ def make_sales_invoice(source_name, target_doc=None):
if automatically_fetch_payment_terms:
doc.set_payment_schedule()
doc.set_onload('ignore_price_list', True)
return doc
@frappe.whitelist()

View File

@@ -785,6 +785,7 @@ def make_purchase_invoice(source_name, target_doc=None):
}
}, target_doc, set_missing_values)
doclist.set_onload('ignore_price_list', True)
return doclist
def get_invoiced_qty_map(purchase_receipt):

View File

@@ -3,6 +3,7 @@
import json
from typing import List, Optional, Union
import frappe
from frappe import ValidationError, _
@@ -591,22 +592,30 @@ def get_delivery_note_serial_no(item_code, qty, delivery_note):
return serial_nos
@frappe.whitelist()
def auto_fetch_serial_number(qty, item_code, warehouse,
posting_date=None, batch_nos=None, for_doctype=None, exclude_sr_nos=None):
def auto_fetch_serial_number(
qty: float,
item_code: str,
warehouse: str,
posting_date: Optional[str] = None,
batch_nos: Optional[Union[str, List[str]]] = None,
for_doctype: Optional[str] = None,
exclude_sr_nos: Optional[List[str]] = None
) -> List[str]:
filters = frappe._dict({"item_code": item_code, "warehouse": warehouse})
if exclude_sr_nos is None:
exclude_sr_nos = []
else:
exclude_sr_nos = safe_json_loads(exclude_sr_nos)
exclude_sr_nos = get_serial_nos(clean_serial_no_string("\n".join(exclude_sr_nos)))
if batch_nos:
batch_nos = safe_json_loads(batch_nos)
if isinstance(batch_nos, list):
filters.batch_no = batch_nos
elif isinstance(batch_nos, str):
filters.batch_no = [batch_nos]
else:
filters.batch_no = [str(batch_nos)]
if posting_date:
filters.expiry_date = posting_date

View File

@@ -274,7 +274,8 @@ class TestSerialNo(FrappeTestCase):
msg=f"{partial_fetch} should be subset of {first_fetch}")
# exclusion
remaining = auto_fetch_serial_number(3, item_code, warehouse, exclude_sr_nos=partial_fetch)
remaining = auto_fetch_serial_number(3, item_code, warehouse,
exclude_sr_nos=json.dumps(partial_fetch))
self.assertEqual(sorted(remaining + partial_fetch), first_fetch)
# batchwise

View File

@@ -52,24 +52,6 @@
</div>
<script>
frappe.ready(() => {
$('.product-filter-filter').on('keydown', frappe.utils.debounce((e) => {
const $input = $(e.target);
const keyword = ($input.val() || '').toLowerCase();
const $filter_options = $input.next('.filter-options');
$filter_options.find('.custom-control').show();
$filter_options.find('.custom-control').each((i, el) => {
const $el = $(el);
const value = $el.data('value').toLowerCase();
if (!value.includes(keyword)) {
$el.hide();
}
});
}, 300));
})
</script>
</div>
</div>
</div>

View File

@@ -300,13 +300,13 @@
{% if values | len > 20 %}
<!-- show inline filter if values more than 20 -->
<input type="text" class="form-control form-control-sm mb-2 product-filter-filter"/>
<input type="text" class="form-control form-control-sm mb-2 filter-lookup-input" placeholder="Search {{ item_field.label + 's' }}"/>
{% endif %}
{% if values %}
<div class="filter-options">
{% for value in values %}
<div class="checkbox" data-value="{{ value }}">
<div class="filter-lookup-wrapper checkbox" data-value="{{ value }}">
<label for="{{value}}">
<input type="checkbox"
class="product-filter field-filter"
@@ -329,17 +329,17 @@
{%- macro attribute_filter_section(filters)-%}
{% for attribute in filters %}
<div class="mb-4 filter-block pb-5">
<div class="filter-label mb-3">{{ attribute.name}}</div>
{% if values | len > 20 %}
<div class="filter-label mb-3">{{ attribute.name }}</div>
{% if attribute.item_attribute_values | len > 20 %}
<!-- show inline filter if values more than 20 -->
<input type="text" class="form-control form-control-sm mb-2 product-filter-filter"/>
<input type="text" class="form-control form-control-sm mb-2 filter-lookup-input" placeholder="Search {{ attribute.name + 's' }}"/>
{% endif %}
{% if attribute.item_attribute_values %}
<div class="filter-options">
{% for attr_value in attribute.item_attribute_values %}
<div class="checkbox">
<label>
<div class="filter-lookup-wrapper checkbox" data-value="{{ attr_value }}">
<label data-value="{{ attr_value }}">
<input type="checkbox"
class="product-filter attribute-filter"
id="{{ attr_value }}"

View File

@@ -31,24 +31,6 @@
{% endif %}
</div>
<script>
frappe.ready(() => {
$('.product-filter-filter').on('keydown', frappe.utils.debounce((e) => {
const $input = $(e.target);
const keyword = ($input.val() || '').toLowerCase();
const $filter_options = $input.next('.filter-options');
$filter_options.find('.custom-control').show();
$filter_options.find('.custom-control').each((i, el) => {
const $el = $(el);
const value = $el.data('value').toLowerCase();
if (!value.includes(keyword)) {
$el.hide();
}
});
}, 300));
})
</script>
</div>
</div>