Merge pull request #26868 from rohitwaghchaure/merge-version-13-hotfix-into-v13-pre-9

chore: merge version-13-hotfix into v13-pre-release
This commit is contained in:
rohitwaghchaure 2021-08-10 01:29:00 +05:30 committed by GitHub
commit 7ca1493655
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
80 changed files with 1833 additions and 2028 deletions

View File

@ -1,16 +1,25 @@
name: Backport name: Backport
on: on:
pull_request: pull_request_target:
types: types:
- closed - closed
- labeled - labeled
jobs: jobs:
backport: main:
runs-on: ubuntu-18.04 runs-on: ubuntu-latest
name: Backport
steps: steps:
- name: Backport - name: Checkout Actions
uses: tibdex/backport@v1 uses: actions/checkout@v2
with: with:
github_token: ${{ secrets.GITHUB_TOKEN }} repository: "frappe/backport"
path: ./actions
ref: develop
- name: Install Actions
run: npm install --production --prefix ./actions
- name: Run backport
uses: ./actions/backport
with:
token: ${{secrets.BACKPORT_BOT_TOKEN}}
labelsToAdd: "backport"
title: "{{originalTitle}}"

View File

@ -21,13 +21,13 @@ erpnext/quality_management/ @marination @rohitwaghchaure
erpnext/shopping_cart/ @marination erpnext/shopping_cart/ @marination
erpnext/stock/ @marination @rohitwaghchaure @ankush erpnext/stock/ @marination @rohitwaghchaure @ankush
erpnext/crm/ @ruchamahabal erpnext/crm/ @ruchamahabal @pateljannat
erpnext/education/ @ruchamahabal erpnext/education/ @ruchamahabal @pateljannat
erpnext/healthcare/ @ruchamahabal erpnext/healthcare/ @ruchamahabal @pateljannat @chillaranand
erpnext/hr/ @ruchamahabal erpnext/hr/ @ruchamahabal @pateljannat
erpnext/non_profit/ @ruchamahabal erpnext/non_profit/ @ruchamahabal
erpnext/payroll @ruchamahabal erpnext/payroll @ruchamahabal @pateljannat
erpnext/projects/ @ruchamahabal erpnext/projects/ @ruchamahabal @pateljannat
erpnext/controllers @deepeshgarg007 @nextchamp-saqib @rohitwaghchaure @marination erpnext/controllers @deepeshgarg007 @nextchamp-saqib @rohitwaghchaure @marination

View File

@ -230,7 +230,7 @@ class Account(NestedSet):
if self.check_gle_exists(): if self.check_gle_exists():
throw(_("Account with existing transaction can not be converted to group.")) throw(_("Account with existing transaction can not be converted to group."))
elif self.account_type and not self.flags.exclude_account_type_check: elif self.account_type and not self.flags.exclude_account_type_check:
throw(_("Cannot covert to Group because Account Type is selected.")) throw(_("Cannot convert to Group because Account Type is selected."))
else: else:
self.is_group = 1 self.is_group = 1
self.save() self.save()

View File

@ -249,7 +249,7 @@ class TestBudget(unittest.TestCase):
def set_total_expense_zero(posting_date, budget_against_field=None, budget_against_CC=None): def set_total_expense_zero(posting_date, budget_against_field=None, budget_against_CC=None):
if budget_against_field == "project": if budget_against_field == "project":
budget_against = "_Test Project" budget_against = frappe.db.get_value("Project", {"project_name": "_Test Project"})
else: else:
budget_against = budget_against_CC or "_Test Cost Center - _TC" budget_against = budget_against_CC or "_Test Cost Center - _TC"
@ -275,7 +275,7 @@ def set_total_expense_zero(posting_date, budget_against_field=None, budget_again
"_Test Bank - _TC", -existing_expense, "_Test Cost Center - _TC", posting_date=nowdate(), submit=True) "_Test Bank - _TC", -existing_expense, "_Test Cost Center - _TC", posting_date=nowdate(), submit=True)
elif budget_against_field == "project": elif budget_against_field == "project":
make_journal_entry("_Test Account Cost for Goods Sold - _TC", make_journal_entry("_Test Account Cost for Goods Sold - _TC",
"_Test Bank - _TC", -existing_expense, "_Test Cost Center - _TC", submit=True, project="_Test Project", posting_date=nowdate()) "_Test Bank - _TC", -existing_expense, "_Test Cost Center - _TC", submit=True, project=budget_against, posting_date=nowdate())
def make_budget(**args): def make_budget(**args):
args = frappe._dict(args) args = frappe._dict(args)

View File

@ -7,6 +7,8 @@ cur_frm.cscript.tax_table = "Advance Taxes and Charges";
frappe.ui.form.on('Payment Entry', { frappe.ui.form.on('Payment Entry', {
onload: function(frm) { onload: function(frm) {
frm.ignore_doctypes_on_cancel_all = ['Sales Invoice', 'Purchase Invoice'];
if(frm.doc.__islocal) { if(frm.doc.__islocal) {
if (!frm.doc.paid_from) frm.set_value("paid_from_account_currency", null); if (!frm.doc.paid_from) frm.set_value("paid_from_account_currency", null);
if (!frm.doc.paid_to) frm.set_value("paid_to_account_currency", null); if (!frm.doc.paid_to) frm.set_value("paid_to_account_currency", null);

View File

@ -1545,6 +1545,7 @@
"fieldname": "consolidated_invoice", "fieldname": "consolidated_invoice",
"fieldtype": "Link", "fieldtype": "Link",
"label": "Consolidated Sales Invoice", "label": "Consolidated Sales Invoice",
"no_copy": 1,
"options": "Sales Invoice", "options": "Sales Invoice",
"read_only": 1 "read_only": 1
} }
@ -1552,7 +1553,7 @@
"icon": "fa fa-file-text", "icon": "fa fa-file-text",
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2021-02-01 15:03:33.800707", "modified": "2021-07-29 13:37:20.636171",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "POS Invoice", "name": "POS Invoice",

View File

@ -558,7 +558,8 @@
"description": "Simple Python Expression, Example: territory != 'All Territories'", "description": "Simple Python Expression, Example: territory != 'All Territories'",
"fieldname": "condition", "fieldname": "condition",
"fieldtype": "Code", "fieldtype": "Code",
"label": "Condition" "label": "Condition",
"options": "PythonExpression"
}, },
{ {
"fieldname": "column_break_42", "fieldname": "column_break_42",
@ -575,7 +576,7 @@
"icon": "fa fa-gift", "icon": "fa fa-gift",
"idx": 1, "idx": 1,
"links": [], "links": [],
"modified": "2021-03-06 22:01:24.840422", "modified": "2021-08-06 15:10:04.219321",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Pricing Rule", "name": "Pricing Rule",

View File

@ -134,7 +134,7 @@ erpnext.accounts.PurchaseInvoice = erpnext.buying.BuyingController.extend({
}, },
get_query_filters: { get_query_filters: {
docstatus: 1, docstatus: 1,
status: ["not in", ["Closed", "Completed"]], status: ["not in", ["Closed", "Completed", "Return Issued"]],
company: me.frm.doc.company, company: me.frm.doc.company,
is_return: 0 is_return: 0
} }

View File

@ -48,6 +48,8 @@
"shipping_address", "shipping_address",
"company_address", "company_address",
"company_address_display", "company_address_display",
"dispatch_address_name",
"dispatch_address",
"currency_and_price_list", "currency_and_price_list",
"currency", "currency",
"conversion_rate", "conversion_rate",
@ -1966,6 +1968,21 @@
"fieldname": "disable_rounded_total", "fieldname": "disable_rounded_total",
"fieldtype": "Check", "fieldtype": "Check",
"label": "Disable Rounded Total" "label": "Disable Rounded Total"
},
{
"allow_on_submit": 1,
"fieldname": "dispatch_address_name",
"fieldtype": "Link",
"label": "Dispatch Address Name",
"options": "Address",
"print_hide": 1
},
{
"allow_on_submit": 1,
"fieldname": "dispatch_address",
"fieldtype": "Small Text",
"label": "Dispatch Address",
"read_only": 1
} }
], ],
"icon": "fa fa-file-text", "icon": "fa fa-file-text",
@ -1978,7 +1995,7 @@
"link_fieldname": "consolidated_invoice" "link_fieldname": "consolidated_invoice"
} }
], ],
"modified": "2021-05-20 22:48:33.988881", "modified": "2021-07-08 14:03:55.502522",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Sales Invoice", "name": "Sales Invoice",

View File

@ -1908,6 +1908,8 @@ class TestSalesInvoice(unittest.TestCase):
self.assertEqual(data['billLists'][0]['sgstValue'], 5400) self.assertEqual(data['billLists'][0]['sgstValue'], 5400)
self.assertEqual(data['billLists'][0]['vehicleNo'], 'KA12KA1234') self.assertEqual(data['billLists'][0]['vehicleNo'], 'KA12KA1234')
self.assertEqual(data['billLists'][0]['itemList'][0]['taxableAmount'], 60000) self.assertEqual(data['billLists'][0]['itemList'][0]['taxableAmount'], 60000)
self.assertEqual(data['billLists'][0]['actualFromStateCode'],7)
self.assertEqual(data['billLists'][0]['fromStateCode'],27)
def test_einvoice_submission_without_irn(self): def test_einvoice_submission_without_irn(self):
# init # init
@ -2062,6 +2064,30 @@ def make_test_address_for_ewaybill():
address.save() address.save()
if not frappe.db.exists('Address', '_Test Dispatch-Address for Eway bill-Shipping'):
address = frappe.get_doc({
"address_line1": "_Test Dispatch Address Line 1",
"address_title": "_Test Dispatch-Address for Eway bill",
"address_type": "Shipping",
"city": "_Test City",
"state": "Test State",
"country": "India",
"doctype": "Address",
"is_primary_address": 0,
"phone": "+910000000000",
"gstin": "07AAACC1206D1ZI",
"gst_state": "Delhi",
"gst_state_number": "07",
"pincode": "1100101"
}).insert()
address.append("links", {
"link_doctype": "Company",
"link_name": "_Test Company"
})
address.save()
def make_test_transporter_for_ewaybill(): def make_test_transporter_for_ewaybill():
if not frappe.db.exists('Supplier', '_Test Transporter'): if not frappe.db.exists('Supplier', '_Test Transporter'):
frappe.get_doc({ frappe.get_doc({
@ -2100,6 +2126,7 @@ def make_sales_invoice_for_ewaybill():
si.distance = 2000 si.distance = 2000
si.company_address = "_Test Address for Eway bill-Billing" si.company_address = "_Test Address for Eway bill-Billing"
si.customer_address = "_Test Customer-Address for Eway bill-Shipping" si.customer_address = "_Test Customer-Address for Eway bill-Shipping"
si.dispatch_address_name = "_Test Dispatch-Address for Eway bill-Shipping"
si.vehicle_no = "KA12KA1234" si.vehicle_no = "KA12KA1234"
si.gst_category = "Registered Regular" si.gst_category = "Registered Regular"
si.mode_of_transport = 'Road' si.mode_of_transport = 'Road'

View File

@ -78,7 +78,7 @@
"label": "Cost" "label": "Cost"
}, },
{ {
"depends_on": "eval:doc.price_determination==\"Based on price list\"", "depends_on": "eval:doc.price_determination==\"Based On Price List\"",
"fieldname": "price_list", "fieldname": "price_list",
"fieldtype": "Link", "fieldtype": "Link",
"label": "Price List", "label": "Price List",
@ -147,7 +147,7 @@
} }
], ],
"links": [], "links": [],
"modified": "2020-06-25 10:53:44.205774", "modified": "2021-08-09 10:53:44.205774",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Subscription Plan", "name": "Subscription Plan",

View File

@ -82,24 +82,46 @@ frappe.ui.form.on('Asset', {
if (in_list(["Submitted", "Partially Depreciated", "Fully Depreciated"], frm.doc.status)) { if (in_list(["Submitted", "Partially Depreciated", "Fully Depreciated"], frm.doc.status)) {
frm.add_custom_button("Transfer Asset", function() { frm.add_custom_button("Transfer Asset", function() {
erpnext.asset.transfer_asset(frm); erpnext.asset.transfer_asset(frm);
}); }, __("Manage"));
frm.add_custom_button("Scrap Asset", function() { frm.add_custom_button("Scrap Asset", function() {
erpnext.asset.scrap_asset(frm); erpnext.asset.scrap_asset(frm);
}); }, __("Manage"));
frm.add_custom_button("Sell Asset", function() { frm.add_custom_button("Sell Asset", function() {
frm.trigger("make_sales_invoice"); frm.trigger("make_sales_invoice");
}); }, __("Manage"));
} else if (frm.doc.status=='Scrapped') { } else if (frm.doc.status=='Scrapped') {
frm.add_custom_button("Restore Asset", function() { frm.add_custom_button("Restore Asset", function() {
erpnext.asset.restore_asset(frm); erpnext.asset.restore_asset(frm);
}); }, __("Manage"));
}
if (frm.doc.maintenance_required && !frm.doc.maintenance_schedule) {
frm.add_custom_button(__("Maintain Asset"), function() {
frm.trigger("create_asset_maintenance");
}, __("Manage"));
}
frm.add_custom_button(__("Repair Asset"), function() {
frm.trigger("create_asset_repair");
}, __("Manage"));
if (frm.doc.status != 'Fully Depreciated') {
frm.add_custom_button(__("Adjust Asset Value"), function() {
frm.trigger("create_asset_adjustment");
}, __("Manage"));
}
if (!frm.doc.calculate_depreciation) {
frm.add_custom_button(__("Create Depreciation Entry"), function() {
frm.trigger("make_journal_entry");
}, __("Manage"));
} }
if (frm.doc.purchase_receipt || !frm.doc.is_existing_asset) { if (frm.doc.purchase_receipt || !frm.doc.is_existing_asset) {
frm.add_custom_button("General Ledger", function() { frm.add_custom_button("View General Ledger", function() {
frappe.route_options = { frappe.route_options = {
"voucher_no": frm.doc.name, "voucher_no": frm.doc.name,
"from_date": frm.doc.available_for_use_date, "from_date": frm.doc.available_for_use_date,
@ -107,27 +129,9 @@ frappe.ui.form.on('Asset', {
"company": frm.doc.company "company": frm.doc.company
}; };
frappe.set_route("query-report", "General Ledger"); frappe.set_route("query-report", "General Ledger");
}); }, __("Manage"));
} }
if (frm.doc.maintenance_required && !frm.doc.maintenance_schedule) {
frm.add_custom_button(__("Asset Maintenance"), function() {
frm.trigger("create_asset_maintenance");
}, __('Create'));
}
if (frm.doc.status != 'Fully Depreciated') {
frm.add_custom_button(__("Asset Value Adjustment"), function() {
frm.trigger("create_asset_adjustment");
}, __('Create'));
}
if (!frm.doc.calculate_depreciation) {
frm.add_custom_button(__("Depreciation Entry"), function() {
frm.trigger("make_journal_entry");
}, __('Create'));
}
frm.page.set_inner_btn_group_as_primary(__('Create'));
frm.trigger("setup_chart"); frm.trigger("setup_chart");
} }
@ -304,6 +308,20 @@ frappe.ui.form.on('Asset', {
}) })
}, },
create_asset_repair: function(frm) {
frappe.call({
args: {
"asset": frm.doc.name,
"asset_name": frm.doc.asset_name
},
method: "erpnext.assets.doctype.asset.asset.create_asset_repair",
callback: function(r) {
var doclist = frappe.model.sync(r.message);
frappe.set_route("Form", doclist[0].doctype, doclist[0].name);
}
});
},
create_asset_adjustment: function(frm) { create_asset_adjustment: function(frm) {
frappe.call({ frappe.call({
args: { args: {

View File

@ -502,7 +502,7 @@
"link_fieldname": "asset" "link_fieldname": "asset"
} }
], ],
"modified": "2021-01-22 12:38:59.091510", "modified": "2021-06-24 14:58:51.097908",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Assets", "module": "Assets",
"name": "Asset", "name": "Asset",

View File

@ -168,17 +168,24 @@ class Asset(AccountsController):
d.precision("rate_of_depreciation")) d.precision("rate_of_depreciation"))
def make_depreciation_schedule(self): def make_depreciation_schedule(self):
if 'Manual' not in [d.depreciation_method for d in self.finance_books]: if 'Manual' not in [d.depreciation_method for d in self.finance_books] and not self.schedules:
self.schedules = [] self.schedules = []
if self.get("schedules") or not self.available_for_use_date: if not self.available_for_use_date:
return return
for d in self.get('finance_books'): for d in self.get('finance_books'):
self.validate_asset_finance_books(d) self.validate_asset_finance_books(d)
start = self.clear_depreciation_schedule()
value_after_depreciation = (flt(self.gross_purchase_amount) - # value_after_depreciation - current Asset value
flt(self.opening_accumulated_depreciation)) if d.value_after_depreciation:
value_after_depreciation = (flt(d.value_after_depreciation) -
flt(self.opening_accumulated_depreciation))
else:
value_after_depreciation = (flt(self.gross_purchase_amount) -
flt(self.opening_accumulated_depreciation))
d.value_after_depreciation = value_after_depreciation d.value_after_depreciation = value_after_depreciation
@ -191,7 +198,7 @@ class Asset(AccountsController):
number_of_pending_depreciations += 1 number_of_pending_depreciations += 1
skip_row = False skip_row = False
for n in range(number_of_pending_depreciations): for n in range(start, number_of_pending_depreciations):
# If depreciation is already completed (for double declining balance) # If depreciation is already completed (for double declining balance)
if skip_row: continue if skip_row: continue
@ -216,11 +223,13 @@ class Asset(AccountsController):
# For last row # For last row
elif has_pro_rata and n == cint(number_of_pending_depreciations) - 1: elif has_pro_rata and n == cint(number_of_pending_depreciations) - 1:
to_date = add_months(self.available_for_use_date, if not self.flags.increase_in_asset_life:
n * cint(d.frequency_of_depreciation)) # In case of increase_in_asset_life, the self.to_date is already set on asset_repair submission
self.to_date = add_months(self.available_for_use_date,
n * cint(d.frequency_of_depreciation))
depreciation_amount, days, months = self.get_pro_rata_amt(d, depreciation_amount, days, months = self.get_pro_rata_amt(d,
depreciation_amount, schedule_date, to_date) depreciation_amount, schedule_date, self.to_date)
monthly_schedule_date = add_months(schedule_date, 1) monthly_schedule_date = add_months(schedule_date, 1)
@ -284,10 +293,23 @@ class Asset(AccountsController):
"finance_book_id": d.idx "finance_book_id": d.idx
}) })
# used when depreciation schedule needs to be modified due to increase in asset life
def clear_depreciation_schedule(self):
start = 0
for n in range(len(self.schedules)):
if not self.schedules[n].journal_entry:
del self.schedules[n:]
start = n
break
return start
# if it returns True, depreciation_amount will not be equal for the first and last rows
def check_is_pro_rata(self, row): def check_is_pro_rata(self, row):
has_pro_rata = False has_pro_rata = False
days = date_diff(row.depreciation_start_date, self.available_for_use_date) + 1 days = date_diff(row.depreciation_start_date, self.available_for_use_date) + 1
# if frequency_of_depreciation is 12 months, total_days = 365
total_days = get_total_days(row.depreciation_start_date, row.frequency_of_depreciation) total_days = get_total_days(row.depreciation_start_date, row.frequency_of_depreciation)
if days < total_days: if days < total_days:
@ -346,11 +368,12 @@ class Asset(AccountsController):
if d.finance_book_id not in finance_books: if d.finance_book_id not in finance_books:
accumulated_depreciation = flt(self.opening_accumulated_depreciation) accumulated_depreciation = flt(self.opening_accumulated_depreciation)
value_after_depreciation = flt(self.get_value_after_depreciation(d.finance_book_id)) value_after_depreciation = flt(self.get_value_after_depreciation(d.finance_book_id))
finance_books.append(d.finance_book_id) finance_books.append(int(d.finance_book_id))
depreciation_amount = flt(d.depreciation_amount, d.precision("depreciation_amount")) depreciation_amount = flt(d.depreciation_amount, d.precision("depreciation_amount"))
value_after_depreciation -= flt(depreciation_amount) value_after_depreciation -= flt(depreciation_amount)
# for the last row, if depreciation method = Straight Line
if straight_line_idx and i == max(straight_line_idx) - 1: if straight_line_idx and i == max(straight_line_idx) - 1:
book = self.get('finance_books')[cint(d.finance_book_id) - 1] book = self.get('finance_books')[cint(d.finance_book_id) - 1]
depreciation_amount += flt(value_after_depreciation - depreciation_amount += flt(value_after_depreciation -
@ -625,9 +648,18 @@ def create_asset_maintenance(asset, item_code, item_name, asset_category, compan
}) })
return asset_maintenance return asset_maintenance
@frappe.whitelist()
def create_asset_repair(asset, asset_name):
asset_repair = frappe.new_doc("Asset Repair")
asset_repair.update({
"asset": asset,
"asset_name": asset_name
})
return asset_repair
@frappe.whitelist() @frappe.whitelist()
def create_asset_adjustment(asset, asset_category, company): def create_asset_adjustment(asset, asset_category, company):
asset_maintenance = frappe.new_doc("Asset Value Adjustment") asset_maintenance = frappe.get_doc("Asset Value Adjustment")
asset_maintenance.update({ asset_maintenance.update({
"asset": asset, "asset": asset,
"company": company, "company": company,
@ -757,9 +789,16 @@ def get_depreciation_amount(asset, depreciable_value, row):
depreciation_left = flt(row.total_number_of_depreciations) - flt(asset.number_of_depreciations_booked) depreciation_left = flt(row.total_number_of_depreciations) - flt(asset.number_of_depreciations_booked)
if row.depreciation_method in ("Straight Line", "Manual"): if row.depreciation_method in ("Straight Line", "Manual"):
depreciation_amount = (flt(row.value_after_depreciation) - # if the Depreciation Schedule is being prepared for the first time
flt(row.expected_value_after_useful_life)) / depreciation_left if not asset.flags.increase_in_asset_life:
depreciation_amount = (flt(row.value_after_depreciation) -
flt(row.expected_value_after_useful_life)) / depreciation_left
# if the Depreciation Schedule is being modified after Asset Repair
else:
depreciation_amount = (flt(row.value_after_depreciation) -
flt(row.expected_value_after_useful_life)) / (date_diff(asset.to_date, asset.available_for_use_date) / 365)
else: else:
depreciation_amount = flt(depreciable_value * (flt(row.rate_of_depreciation) / 100)) depreciation_amount = flt(depreciable_value * (flt(row.rate_of_depreciation) / 100))
return depreciation_amount return depreciation_amount

View File

@ -125,7 +125,6 @@ class TestAsset(unittest.TestCase):
"frequency_of_depreciation": 12, "frequency_of_depreciation": 12,
"depreciation_start_date": "2030-12-31" "depreciation_start_date": "2030-12-31"
}) })
asset.insert()
self.assertEqual(asset.status, "Draft") self.assertEqual(asset.status, "Draft")
asset.save() asset.save()
expected_schedules = [ expected_schedules = [
@ -154,9 +153,8 @@ class TestAsset(unittest.TestCase):
"frequency_of_depreciation": 12, "frequency_of_depreciation": 12,
"depreciation_start_date": '2030-12-31' "depreciation_start_date": '2030-12-31'
}) })
asset.insert()
self.assertEqual(asset.status, "Draft")
asset.save() asset.save()
self.assertEqual(asset.status, "Draft")
expected_schedules = [ expected_schedules = [
['2030-12-31', 66667.00, 66667.00], ['2030-12-31', 66667.00, 66667.00],
@ -185,7 +183,7 @@ class TestAsset(unittest.TestCase):
"frequency_of_depreciation": 12, "frequency_of_depreciation": 12,
"depreciation_start_date": "2030-12-31" "depreciation_start_date": "2030-12-31"
}) })
asset.insert() asset.save()
self.assertEqual(asset.status, "Draft") self.assertEqual(asset.status, "Draft")
expected_schedules = [ expected_schedules = [
@ -216,7 +214,6 @@ class TestAsset(unittest.TestCase):
"depreciation_start_date": "2030-12-31" "depreciation_start_date": "2030-12-31"
}) })
asset.insert()
asset.save() asset.save()
expected_schedules = [ expected_schedules = [
@ -247,7 +244,6 @@ class TestAsset(unittest.TestCase):
"frequency_of_depreciation": 10, "frequency_of_depreciation": 10,
"depreciation_start_date": "2020-12-31" "depreciation_start_date": "2020-12-31"
}) })
asset.insert()
asset.submit() asset.submit()
asset.load_from_db() asset.load_from_db()
self.assertEqual(asset.status, "Submitted") self.assertEqual(asset.status, "Submitted")
@ -350,7 +346,6 @@ class TestAsset(unittest.TestCase):
"frequency_of_depreciation": 10, "frequency_of_depreciation": 10,
"depreciation_start_date": "2020-12-31" "depreciation_start_date": "2020-12-31"
}) })
asset.insert()
asset.submit() asset.submit()
post_depreciation_entries(date="2021-01-01") post_depreciation_entries(date="2021-01-01")
@ -380,7 +375,6 @@ class TestAsset(unittest.TestCase):
"total_number_of_depreciations": 10, "total_number_of_depreciations": 10,
"frequency_of_depreciation": 1 "frequency_of_depreciation": 1
}) })
asset.insert()
asset.submit() asset.submit()
post_depreciation_entries(date=add_months('2020-01-01', 4)) post_depreciation_entries(date=add_months('2020-01-01', 4))
@ -424,7 +418,6 @@ class TestAsset(unittest.TestCase):
"frequency_of_depreciation": 10, "frequency_of_depreciation": 10,
"depreciation_start_date": "2020-12-31" "depreciation_start_date": "2020-12-31"
}) })
asset.insert()
asset.submit() asset.submit()
post_depreciation_entries(date="2021-01-01") post_depreciation_entries(date="2021-01-01")
@ -468,7 +461,7 @@ class TestAsset(unittest.TestCase):
"total_number_of_depreciations": 3, "total_number_of_depreciations": 3,
"frequency_of_depreciation": 10 "frequency_of_depreciation": 10
}) })
asset.insert() asset.save()
accumulated_depreciation_after_full_schedule = \ accumulated_depreciation_after_full_schedule = \
max(d.accumulated_depreciation_amount for d in asset.get("schedules")) max(d.accumulated_depreciation_amount for d in asset.get("schedules"))
@ -699,7 +692,7 @@ def create_asset(**args):
"item_code": args.item_code or "Macbook Pro", "item_code": args.item_code or "Macbook Pro",
"company": args.company or"_Test Company", "company": args.company or"_Test Company",
"purchase_date": "2015-01-01", "purchase_date": "2015-01-01",
"calculate_depreciation": 0, "calculate_depreciation": args.calculate_depreciation or 0,
"gross_purchase_amount": 100000, "gross_purchase_amount": 100000,
"purchase_receipt_amount": 100000, "purchase_receipt_amount": 100000,
"expected_value_after_useful_life": 10000, "expected_value_after_useful_life": 10000,
@ -707,9 +700,16 @@ def create_asset(**args):
"available_for_use_date": "2020-06-06", "available_for_use_date": "2020-06-06",
"location": "Test Location", "location": "Test Location",
"asset_owner": "Company", "asset_owner": "Company",
"is_existing_asset": args.is_existing_asset or 0 "is_existing_asset": 1
}) })
if asset.calculate_depreciation:
asset.append("finance_books", {
"depreciation_method": "Straight Line",
"frequency_of_depreciation": 12,
"total_number_of_depreciations": 5
})
try: try:
asset.save() asset.save()
except frappe.DuplicateEntryError: except frappe.DuplicateEntryError:

View File

@ -67,7 +67,6 @@
{ {
"fieldname": "value_after_depreciation", "fieldname": "value_after_depreciation",
"fieldtype": "Currency", "fieldtype": "Currency",
"hidden": 1,
"label": "Value After Depreciation", "label": "Value After Depreciation",
"no_copy": 1, "no_copy": 1,
"options": "Company:company:default_currency", "options": "Company:company:default_currency",
@ -85,7 +84,7 @@
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2020-11-05 16:30:09.213479", "modified": "2021-06-17 12:59:05.743683",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Assets", "module": "Assets",
"name": "Asset Finance Book", "name": "Asset Finance Book",

View File

@ -2,6 +2,45 @@
// For license information, please see license.txt // For license information, please see license.txt
frappe.ui.form.on('Asset Repair', { frappe.ui.form.on('Asset Repair', {
setup: function(frm) {
frm.fields_dict.cost_center.get_query = function(doc) {
return {
filters: {
'is_group': 0,
'company': doc.company
}
};
};
frm.fields_dict.project.get_query = function(doc) {
return {
filters: {
'company': doc.company
}
};
};
frm.fields_dict.warehouse.get_query = function(doc) {
return {
filters: {
'is_group': 0,
'company': doc.company
}
};
};
},
refresh: function(frm) {
if (frm.doc.docstatus) {
frm.add_custom_button("View General Ledger", function() {
frappe.route_options = {
"voucher_no": frm.doc.name
};
frappe.set_route("query-report", "General Ledger");
});
}
},
repair_status: (frm) => { repair_status: (frm) => {
if (frm.doc.completion_date && frm.doc.repair_status == "Completed") { if (frm.doc.completion_date && frm.doc.repair_status == "Completed") {
frappe.call ({ frappe.call ({
@ -17,5 +56,16 @@ frappe.ui.form.on('Asset Repair', {
} }
}); });
} }
if (frm.doc.repair_status == "Completed") {
frm.set_value('completion_date', frappe.datetime.now_datetime());
}
} }
}); });
frappe.ui.form.on('Asset Repair Consumed Item', {
consumed_quantity: function(frm, cdt, cdn) {
var row = locals[cdt][cdn];
frappe.model.set_value(cdt, cdn, 'total_value', row.consumed_quantity * row.valuation_rate);
},
});

View File

@ -7,38 +7,43 @@
"editable_grid": 1, "editable_grid": 1,
"engine": "InnoDB", "engine": "InnoDB",
"field_order": [ "field_order": [
"naming_series", "asset",
"asset_name", "company",
"column_break_2", "column_break_2",
"item_code", "asset_name",
"item_name", "naming_series",
"section_break_5", "section_break_5",
"failure_date", "failure_date",
"assign_to", "repair_status",
"assign_to_name",
"column_break_6", "column_break_6",
"completion_date", "completion_date",
"repair_status", "accounting_dimensions_section",
"cost_center",
"column_break_14",
"project",
"accounting_details",
"repair_cost", "repair_cost",
"capitalize_repair_cost",
"stock_consumption",
"column_break_8",
"purchase_invoice",
"stock_consumption_details_section",
"warehouse",
"stock_items",
"total_repair_cost",
"stock_entry",
"asset_depreciation_details_section",
"increase_in_asset_life",
"section_break_9", "section_break_9",
"description", "description",
"column_break_9", "column_break_9",
"actions_performed", "actions_performed",
"section_break_17", "section_break_23",
"downtime", "downtime",
"column_break_19", "column_break_19",
"amended_from" "amended_from"
], ],
"fields": [ "fields": [
{
"columns": 1,
"fieldname": "asset_name",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Asset",
"options": "Asset",
"reqd": 1
},
{ {
"fieldname": "naming_series", "fieldname": "naming_series",
"fieldtype": "Select", "fieldtype": "Select",
@ -50,18 +55,6 @@
"fieldname": "column_break_2", "fieldname": "column_break_2",
"fieldtype": "Column Break" "fieldtype": "Column Break"
}, },
{
"fetch_from": "asset_name.item_code",
"fieldname": "item_code",
"fieldtype": "Read Only",
"label": "Item Code"
},
{
"fetch_from": "asset_name.item_name",
"fieldname": "item_name",
"fieldtype": "Read Only",
"label": "Item Name"
},
{ {
"fieldname": "section_break_5", "fieldname": "section_break_5",
"fieldtype": "Section Break", "fieldtype": "Section Break",
@ -74,33 +67,20 @@
"label": "Failure Date", "label": "Failure Date",
"reqd": 1 "reqd": 1
}, },
{
"allow_on_submit": 1,
"fieldname": "assign_to",
"fieldtype": "Link",
"label": "Assign To",
"options": "User"
},
{
"allow_on_submit": 1,
"fetch_from": "assign_to.full_name",
"fieldname": "assign_to_name",
"fieldtype": "Read Only",
"label": "Assign To Name"
},
{ {
"fieldname": "column_break_6", "fieldname": "column_break_6",
"fieldtype": "Column Break" "fieldtype": "Column Break"
}, },
{ {
"allow_on_submit": 1, "depends_on": "eval:!doc.__islocal",
"fieldname": "completion_date", "fieldname": "completion_date",
"fieldtype": "Datetime", "fieldtype": "Datetime",
"label": "Completion Date" "label": "Completion Date",
"no_copy": 1
}, },
{ {
"allow_on_submit": 1,
"default": "Pending", "default": "Pending",
"depends_on": "eval:!doc.__islocal",
"fieldname": "repair_status", "fieldname": "repair_status",
"fieldtype": "Select", "fieldtype": "Select",
"label": "Repair Status", "label": "Repair Status",
@ -116,25 +96,18 @@
{ {
"fieldname": "description", "fieldname": "description",
"fieldtype": "Long Text", "fieldtype": "Long Text",
"label": "Error Description", "label": "Error Description"
"reqd": 1
}, },
{ {
"fieldname": "column_break_9", "fieldname": "column_break_9",
"fieldtype": "Column Break" "fieldtype": "Column Break"
}, },
{ {
"allow_on_submit": 1,
"fieldname": "actions_performed", "fieldname": "actions_performed",
"fieldtype": "Long Text", "fieldtype": "Long Text",
"label": "Actions performed" "label": "Actions performed"
}, },
{ {
"fieldname": "section_break_17",
"fieldtype": "Section Break"
},
{
"allow_on_submit": 1,
"fieldname": "downtime", "fieldname": "downtime",
"fieldtype": "Data", "fieldtype": "Data",
"in_list_view": 1, "in_list_view": 1,
@ -146,7 +119,7 @@
"fieldtype": "Column Break" "fieldtype": "Column Break"
}, },
{ {
"allow_on_submit": 1, "default": "0",
"fieldname": "repair_cost", "fieldname": "repair_cost",
"fieldtype": "Currency", "fieldtype": "Currency",
"label": "Repair Cost" "label": "Repair Cost"
@ -159,12 +132,138 @@
"options": "Asset Repair", "options": "Asset Repair",
"print_hide": 1, "print_hide": 1,
"read_only": 1 "read_only": 1
},
{
"columns": 1,
"fieldname": "asset",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Asset",
"options": "Asset",
"reqd": 1
},
{
"fetch_from": "asset.asset_name",
"fieldname": "asset_name",
"fieldtype": "Read Only",
"label": "Asset Name"
},
{
"fieldname": "column_break_8",
"fieldtype": "Column Break"
},
{
"default": "0",
"depends_on": "eval:!doc.__islocal",
"fieldname": "capitalize_repair_cost",
"fieldtype": "Check",
"label": "Capitalize Repair Cost"
},
{
"fieldname": "accounting_details",
"fieldtype": "Section Break",
"label": "Accounting Details"
},
{
"fieldname": "stock_items",
"fieldtype": "Table",
"label": "Stock Items",
"mandatory_depends_on": "stock_consumption",
"options": "Asset Repair Consumed Item"
},
{
"fieldname": "section_break_23",
"fieldtype": "Section Break"
},
{
"fieldname": "accounting_dimensions_section",
"fieldtype": "Section Break",
"label": "Accounting Dimensions"
},
{
"fieldname": "cost_center",
"fieldtype": "Link",
"label": "Cost Center",
"options": "Cost Center"
},
{
"fieldname": "project",
"fieldtype": "Link",
"label": "Project",
"options": "Project"
},
{
"fieldname": "column_break_14",
"fieldtype": "Column Break"
},
{
"default": "0",
"depends_on": "eval:!doc.__islocal",
"fieldname": "stock_consumption",
"fieldtype": "Check",
"label": "Stock Consumed During Repair"
},
{
"depends_on": "stock_consumption",
"fieldname": "stock_consumption_details_section",
"fieldtype": "Section Break",
"label": "Stock Consumption Details"
},
{
"depends_on": "eval: doc.stock_consumption && doc.total_repair_cost > 0",
"description": "Sum of Repair Cost and Value of Consumed Stock Items.",
"fieldname": "total_repair_cost",
"fieldtype": "Currency",
"label": "Total Repair Cost",
"read_only": 1
},
{
"depends_on": "stock_consumption",
"fieldname": "warehouse",
"fieldtype": "Link",
"label": "Warehouse",
"options": "Warehouse"
},
{
"depends_on": "capitalize_repair_cost",
"fieldname": "asset_depreciation_details_section",
"fieldtype": "Section Break",
"label": "Asset Depreciation Details"
},
{
"fieldname": "increase_in_asset_life",
"fieldtype": "Int",
"label": "Increase In Asset Life(Months)",
"no_copy": 1
},
{
"depends_on": "eval:!doc.__islocal",
"fieldname": "purchase_invoice",
"fieldtype": "Link",
"label": "Purchase Invoice",
"mandatory_depends_on": "eval: doc.repair_status == 'Completed' && doc.repair_cost > 0",
"no_copy": 1,
"options": "Purchase Invoice"
},
{
"fetch_from": "asset.company",
"fieldname": "company",
"fieldtype": "Link",
"label": "Company",
"options": "Company"
},
{
"fieldname": "stock_entry",
"fieldtype": "Link",
"label": "Stock Entry",
"options": "Stock Entry",
"read_only": 1
} }
], ],
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2021-01-22 15:08:12.495850", "modified": "2021-06-25 13:14:38.307723",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Assets", "module": "Assets",
"name": "Asset Repair", "name": "Asset Repair",
@ -203,6 +302,7 @@
], ],
"sort_field": "modified", "sort_field": "modified",
"sort_order": "DESC", "sort_order": "DESC",
"title_field": "asset_name",
"track_changes": 1, "track_changes": 1,
"track_seen": 1 "track_seen": 1
} }

View File

@ -5,16 +5,252 @@
from __future__ import unicode_literals from __future__ import unicode_literals
import frappe import frappe
from frappe import _ from frappe import _
from frappe.utils import time_diff_in_hours from frappe.utils import time_diff_in_hours, getdate, add_months, flt, cint
from frappe.model.document import Document from erpnext.accounts.general_ledger import make_gl_entries
from erpnext.assets.doctype.asset.asset import get_asset_account
from erpnext.controllers.accounts_controller import AccountsController
class AssetRepair(Document): class AssetRepair(AccountsController):
def validate(self): def validate(self):
if self.repair_status == "Completed" and not self.completion_date: self.asset_doc = frappe.get_doc('Asset', self.asset)
frappe.throw(_("Please select Completion Date for Completed Repair")) self.update_status()
if self.get('stock_items'):
self.set_total_value()
self.calculate_total_repair_cost()
def update_status(self):
if self.repair_status == 'Pending':
frappe.db.set_value('Asset', self.asset, 'status', 'Out of Order')
else:
self.asset_doc.set_status()
def set_total_value(self):
for item in self.get('stock_items'):
item.total_value = flt(item.valuation_rate) * flt(item.consumed_quantity)
def calculate_total_repair_cost(self):
self.total_repair_cost = flt(self.repair_cost)
total_value_of_stock_consumed = self.get_total_value_of_stock_consumed()
self.total_repair_cost += total_value_of_stock_consumed
def before_submit(self):
self.check_repair_status()
if self.get('stock_consumption') or self.get('capitalize_repair_cost'):
self.increase_asset_value()
if self.get('stock_consumption'):
self.check_for_stock_items_and_warehouse()
self.decrease_stock_quantity()
if self.get('capitalize_repair_cost'):
self.make_gl_entries()
if frappe.db.get_value('Asset', self.asset, 'calculate_depreciation') and self.increase_in_asset_life:
self.modify_depreciation_schedule()
self.asset_doc.flags.ignore_validate_update_after_submit = True
self.asset_doc.prepare_depreciation_data()
self.asset_doc.save()
def before_cancel(self):
self.asset_doc = frappe.get_doc('Asset', self.asset)
if self.get('stock_consumption') or self.get('capitalize_repair_cost'):
self.decrease_asset_value()
if self.get('stock_consumption'):
self.increase_stock_quantity()
if self.get('capitalize_repair_cost'):
self.ignore_linked_doctypes = ('GL Entry', 'Stock Ledger Entry')
self.make_gl_entries(cancel=True)
if frappe.db.get_value('Asset', self.asset, 'calculate_depreciation') and self.increase_in_asset_life:
self.revert_depreciation_schedule_on_cancellation()
self.asset_doc.flags.ignore_validate_update_after_submit = True
self.asset_doc.prepare_depreciation_data()
self.asset_doc.save()
def check_repair_status(self):
if self.repair_status == "Pending":
frappe.throw(_("Please update Repair Status."))
def check_for_stock_items_and_warehouse(self):
if not self.get('stock_items'):
frappe.throw(_("Please enter Stock Items consumed during the Repair."), title=_("Missing Items"))
if not self.warehouse:
frappe.throw(_("Please enter Warehouse from which Stock Items consumed during the Repair were taken."), title=_("Missing Warehouse"))
def increase_asset_value(self):
total_value_of_stock_consumed = self.get_total_value_of_stock_consumed()
if self.asset_doc.calculate_depreciation:
for row in self.asset_doc.finance_books:
row.value_after_depreciation += total_value_of_stock_consumed
if self.capitalize_repair_cost:
row.value_after_depreciation += self.repair_cost
def decrease_asset_value(self):
total_value_of_stock_consumed = self.get_total_value_of_stock_consumed()
if self.asset_doc.calculate_depreciation:
for row in self.asset_doc.finance_books:
row.value_after_depreciation -= total_value_of_stock_consumed
if self.capitalize_repair_cost:
row.value_after_depreciation -= self.repair_cost
def get_total_value_of_stock_consumed(self):
total_value_of_stock_consumed = 0
if self.get('stock_consumption'):
for item in self.get('stock_items'):
total_value_of_stock_consumed += item.total_value
return total_value_of_stock_consumed
def decrease_stock_quantity(self):
stock_entry = frappe.get_doc({
"doctype": "Stock Entry",
"stock_entry_type": "Material Issue",
"company": self.company
})
for stock_item in self.get('stock_items'):
stock_entry.append('items', {
"s_warehouse": self.warehouse,
"item_code": stock_item.item,
"qty": stock_item.consumed_quantity,
"basic_rate": stock_item.valuation_rate
})
stock_entry.insert()
stock_entry.submit()
self.db_set('stock_entry', stock_entry.name)
def increase_stock_quantity(self):
stock_entry = frappe.get_doc('Stock Entry', self.stock_entry)
stock_entry.flags.ignore_links = True
stock_entry.cancel()
def make_gl_entries(self, cancel=False):
if flt(self.repair_cost) > 0:
gl_entries = self.get_gl_entries()
make_gl_entries(gl_entries, cancel)
def get_gl_entries(self):
gl_entries = []
repair_and_maintenance_account = frappe.db.get_value('Company', self.company, 'repair_and_maintenance_account')
fixed_asset_account = get_asset_account("fixed_asset_account", asset=self.asset, company=self.company)
expense_account = frappe.get_doc('Purchase Invoice', self.purchase_invoice).items[0].expense_account
gl_entries.append(
self.get_gl_dict({
"account": expense_account,
"credit": self.repair_cost,
"credit_in_account_currency": self.repair_cost,
"against": repair_and_maintenance_account,
"voucher_type": self.doctype,
"voucher_no": self.name,
"cost_center": self.cost_center,
"posting_date": getdate(),
"company": self.company
}, item=self)
)
if self.get('stock_consumption'):
# creating GL Entries for each row in Stock Items based on the Stock Entry created for it
stock_entry = frappe.get_doc('Stock Entry', self.stock_entry)
for item in stock_entry.items:
gl_entries.append(
self.get_gl_dict({
"account": item.expense_account,
"credit": item.amount,
"credit_in_account_currency": item.amount,
"against": repair_and_maintenance_account,
"voucher_type": self.doctype,
"voucher_no": self.name,
"cost_center": self.cost_center,
"posting_date": getdate(),
"company": self.company
}, item=self)
)
gl_entries.append(
self.get_gl_dict({
"account": fixed_asset_account,
"debit": self.total_repair_cost,
"debit_in_account_currency": self.total_repair_cost,
"against": expense_account,
"voucher_type": self.doctype,
"voucher_no": self.name,
"cost_center": self.cost_center,
"posting_date": getdate(),
"against_voucher_type": "Purchase Invoice",
"against_voucher": self.purchase_invoice,
"company": self.company
}, item=self)
)
return gl_entries
def modify_depreciation_schedule(self):
for row in self.asset_doc.finance_books:
row.total_number_of_depreciations += self.increase_in_asset_life/row.frequency_of_depreciation
self.asset_doc.flags.increase_in_asset_life = False
extra_months = self.increase_in_asset_life % row.frequency_of_depreciation
if extra_months != 0:
self.calculate_last_schedule_date(self.asset_doc, row, extra_months)
# to help modify depreciation schedule when increase_in_asset_life is not a multiple of frequency_of_depreciation
def calculate_last_schedule_date(self, asset, row, extra_months):
asset.flags.increase_in_asset_life = True
number_of_pending_depreciations = cint(row.total_number_of_depreciations) - \
cint(asset.number_of_depreciations_booked)
# the Schedule Date in the final row of the old Depreciation Schedule
last_schedule_date = asset.schedules[len(asset.schedules)-1].schedule_date
# the Schedule Date in the final row of the new Depreciation Schedule
asset.to_date = add_months(last_schedule_date, extra_months)
# the latest possible date at which the depreciation can occur, without increasing the Total Number of Depreciations
# if depreciations happen yearly and the Depreciation Posting Date is 01-01-2020, this could be 01-01-2021, 01-01-2022...
schedule_date = add_months(row.depreciation_start_date,
number_of_pending_depreciations * cint(row.frequency_of_depreciation))
if asset.to_date > schedule_date:
row.total_number_of_depreciations += 1
def revert_depreciation_schedule_on_cancellation(self):
for row in self.asset_doc.finance_books:
row.total_number_of_depreciations -= self.increase_in_asset_life/row.frequency_of_depreciation
self.asset_doc.flags.increase_in_asset_life = False
extra_months = self.increase_in_asset_life % row.frequency_of_depreciation
if extra_months != 0:
self.calculate_last_schedule_date_before_modification(self.asset_doc, row, extra_months)
def calculate_last_schedule_date_before_modification(self, asset, row, extra_months):
asset.flags.increase_in_asset_life = True
number_of_pending_depreciations = cint(row.total_number_of_depreciations) - \
cint(asset.number_of_depreciations_booked)
# the Schedule Date in the final row of the modified Depreciation Schedule
last_schedule_date = asset.schedules[len(asset.schedules)-1].schedule_date
# the Schedule Date in the final row of the original Depreciation Schedule
asset.to_date = add_months(last_schedule_date, -extra_months)
# the latest possible date at which the depreciation can occur, without decreasing the Total Number of Depreciations
# if depreciations happen yearly and the Depreciation Posting Date is 01-01-2020, this could be 01-01-2021, 01-01-2022...
schedule_date = add_months(row.depreciation_start_date,
(number_of_pending_depreciations - 1) * cint(row.frequency_of_depreciation))
if asset.to_date < schedule_date:
row.total_number_of_depreciations -= 1
@frappe.whitelist() @frappe.whitelist()
def get_downtime(failure_date, completion_date): def get_downtime(failure_date, completion_date):
downtime = time_diff_in_hours(completion_date, failure_date) downtime = time_diff_in_hours(completion_date, failure_date)
return round(downtime, 2) return round(downtime, 2)

View File

@ -2,8 +2,167 @@
# Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and Contributors # Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt # See license.txt
from __future__ import unicode_literals from __future__ import unicode_literals
import frappe
from frappe.utils import nowdate, flt
import unittest import unittest
from erpnext.assets.doctype.asset.test_asset import create_asset_data, create_asset, set_depreciation_settings_in_company
class TestAssetRepair(unittest.TestCase): class TestAssetRepair(unittest.TestCase):
pass def setUp(self):
set_depreciation_settings_in_company()
create_asset_data()
frappe.db.sql("delete from `tabTax Rule`")
def test_update_status(self):
asset = create_asset()
initial_status = asset.status
asset_repair = create_asset_repair(asset = asset)
if asset_repair.repair_status == "Pending":
asset.reload()
self.assertEqual(asset.status, "Out of Order")
asset_repair.repair_status = "Completed"
asset_repair.save()
asset_status = frappe.db.get_value("Asset", asset_repair.asset, "status")
self.assertEqual(asset_status, initial_status)
def test_stock_item_total_value(self):
asset_repair = create_asset_repair(stock_consumption = 1)
for item in asset_repair.stock_items:
total_value = flt(item.valuation_rate) * flt(item.consumed_quantity)
self.assertEqual(item.total_value, total_value)
def test_total_repair_cost(self):
asset_repair = create_asset_repair(stock_consumption = 1)
total_repair_cost = asset_repair.repair_cost
self.assertEqual(total_repair_cost, asset_repair.repair_cost)
for item in asset_repair.stock_items:
total_repair_cost += item.total_value
self.assertEqual(total_repair_cost, asset_repair.total_repair_cost)
def test_repair_status_after_submit(self):
asset_repair = create_asset_repair(submit = 1)
self.assertNotEqual(asset_repair.repair_status, "Pending")
def test_stock_items(self):
asset_repair = create_asset_repair(stock_consumption = 1)
self.assertTrue(asset_repair.stock_consumption)
self.assertTrue(asset_repair.stock_items)
def test_warehouse(self):
asset_repair = create_asset_repair(stock_consumption = 1)
self.assertTrue(asset_repair.stock_consumption)
self.assertTrue(asset_repair.warehouse)
def test_decrease_stock_quantity(self):
asset_repair = create_asset_repair(stock_consumption = 1, submit = 1)
stock_entry = frappe.get_last_doc('Stock Entry')
self.assertEqual(stock_entry.stock_entry_type, "Material Issue")
self.assertEqual(stock_entry.items[0].s_warehouse, asset_repair.warehouse)
self.assertEqual(stock_entry.items[0].item_code, asset_repair.stock_items[0].item)
self.assertEqual(stock_entry.items[0].qty, asset_repair.stock_items[0].consumed_quantity)
def test_increase_in_asset_value_due_to_stock_consumption(self):
asset = create_asset(calculate_depreciation = 1)
initial_asset_value = get_asset_value(asset)
asset_repair = create_asset_repair(asset= asset, stock_consumption = 1, submit = 1)
asset.reload()
increase_in_asset_value = get_asset_value(asset) - initial_asset_value
self.assertEqual(asset_repair.stock_items[0].total_value, increase_in_asset_value)
def test_increase_in_asset_value_due_to_repair_cost_capitalisation(self):
asset = create_asset(calculate_depreciation = 1)
initial_asset_value = get_asset_value(asset)
asset_repair = create_asset_repair(asset= asset, capitalize_repair_cost = 1, submit = 1)
asset.reload()
increase_in_asset_value = get_asset_value(asset) - initial_asset_value
self.assertEqual(asset_repair.repair_cost, increase_in_asset_value)
def test_purchase_invoice(self):
asset_repair = create_asset_repair(capitalize_repair_cost = 1, submit = 1)
self.assertTrue(asset_repair.purchase_invoice)
def test_gl_entries(self):
asset_repair = create_asset_repair(capitalize_repair_cost = 1, submit = 1)
gl_entry = frappe.get_last_doc('GL Entry')
self.assertEqual(asset_repair.name, gl_entry.voucher_no)
def test_increase_in_asset_life(self):
asset = create_asset(calculate_depreciation = 1)
initial_num_of_depreciations = num_of_depreciations(asset)
create_asset_repair(asset= asset, capitalize_repair_cost = 1, submit = 1)
asset.reload()
self.assertEqual((initial_num_of_depreciations + 1), num_of_depreciations(asset))
self.assertEqual(asset.schedules[-1].accumulated_depreciation_amount, asset.finance_books[0].value_after_depreciation)
def get_asset_value(asset):
return asset.finance_books[0].value_after_depreciation
def num_of_depreciations(asset):
return asset.finance_books[0].total_number_of_depreciations
def create_asset_repair(**args):
from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse
from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice
args = frappe._dict(args)
if args.asset:
asset = args.asset
else:
asset = create_asset(is_existing_asset = 1)
asset_repair = frappe.new_doc("Asset Repair")
asset_repair.update({
"asset": asset.name,
"asset_name": asset.asset_name,
"failure_date": nowdate(),
"description": "Test Description",
"repair_cost": 0,
"company": asset.company
})
if args.stock_consumption:
asset_repair.stock_consumption = 1
asset_repair.warehouse = create_warehouse("Test Warehouse", company = asset.company)
asset_repair.append("stock_items", {
"item": args.item or args.item_code or "_Test Item",
"valuation_rate": args.rate if args.get("rate") is not None else 100,
"consumed_quantity": args.qty or 1
})
asset_repair.insert(ignore_if_duplicate=True)
if args.submit:
asset_repair.repair_status = "Completed"
asset_repair.cost_center = "_Test Cost Center - _TC"
if args.stock_consumption:
stock_entry = frappe.get_doc({
"doctype": "Stock Entry",
"stock_entry_type": "Material Receipt",
"company": asset.company
})
stock_entry.append('items', {
"t_warehouse": asset_repair.warehouse,
"item_code": asset_repair.stock_items[0].item,
"qty": asset_repair.stock_items[0].consumed_quantity
})
stock_entry.submit()
if args.capitalize_repair_cost:
asset_repair.capitalize_repair_cost = 1
asset_repair.repair_cost = 1000
if asset.calculate_depreciation:
asset_repair.increase_in_asset_life = 12
asset_repair.purchase_invoice = make_purchase_invoice().name
asset_repair.submit()
return asset_repair

View File

@ -0,0 +1,55 @@
{
"actions": [],
"creation": "2021-05-12 02:41:54.161024",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"item",
"valuation_rate",
"consumed_quantity",
"total_value"
],
"fields": [
{
"fieldname": "item",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Item",
"options": "Item"
},
{
"fetch_from": "item.valuation_rate",
"fieldname": "valuation_rate",
"fieldtype": "Currency",
"in_list_view": 1,
"label": "Valuation Rate",
"read_only": 1
},
{
"fieldname": "consumed_quantity",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Consumed Quantity"
},
{
"fieldname": "total_value",
"fieldtype": "Currency",
"in_list_view": 1,
"label": "Total Value",
"read_only": 1
}
],
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2021-05-12 03:19:55.006300",
"modified_by": "Administrator",
"module": "Assets",
"name": "Asset Repair Consumed Item",
"owner": "Administrator",
"permissions": [],
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
}

View File

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

View File

@ -1507,7 +1507,7 @@ def set_child_tax_template_and_map(item, child_item, parent_doc):
if child_item.get("item_tax_template"): if child_item.get("item_tax_template"):
child_item.item_tax_rate = get_item_tax_map(parent_doc.get('company'), child_item.item_tax_template, as_json=True) child_item.item_tax_rate = get_item_tax_map(parent_doc.get('company'), child_item.item_tax_template, as_json=True)
def add_taxes_from_tax_template(child_item, parent_doc): def add_taxes_from_tax_template(child_item, parent_doc, db_insert=True):
add_taxes_from_item_tax_template = frappe.db.get_single_value("Accounts Settings", "add_taxes_from_item_tax_template") add_taxes_from_item_tax_template = frappe.db.get_single_value("Accounts Settings", "add_taxes_from_item_tax_template")
if child_item.get("item_tax_rate") and add_taxes_from_item_tax_template: if child_item.get("item_tax_rate") and add_taxes_from_item_tax_template:
@ -1530,7 +1530,8 @@ def add_taxes_from_tax_template(child_item, parent_doc):
"category" : "Total", "category" : "Total",
"add_deduct_tax" : "Add" "add_deduct_tax" : "Add"
}) })
tax_row.db_insert() if db_insert:
tax_row.db_insert()
def set_order_defaults(parent_doctype, parent_doctype_name, child_doctype, child_docname, trans_item): def set_order_defaults(parent_doctype, parent_doctype_name, child_doctype, child_docname, trans_item):
""" """

View File

@ -53,6 +53,13 @@ frappe.ui.form.on("Opportunity", {
frm.get_field("items").grid.set_multiple_add("item_code", "qty"); frm.get_field("items").grid.set_multiple_add("item_code", "qty");
}, },
status:function(frm){
if (frm.doc.status == "Lost"){
frm.trigger('set_as_lost_dialog');
}
},
customer_address: function(frm, cdt, cdn) { customer_address: function(frm, cdt, cdn) {
erpnext.utils.get_address_display(frm, 'customer_address', 'address_display', false); erpnext.utils.get_address_display(frm, 'customer_address', 'address_display', false);
}, },
@ -91,11 +98,6 @@ frappe.ui.form.on("Opportunity", {
frm.add_custom_button(__('Quotation'), frm.add_custom_button(__('Quotation'),
cur_frm.cscript.create_quotation, __('Create')); cur_frm.cscript.create_quotation, __('Create'));
if(doc.status!=="Quotation") {
frm.add_custom_button(__('Lost'), () => {
frm.trigger('set_as_lost_dialog');
});
}
} }
if(!frm.doc.__islocal && frm.perm[0].write && frm.doc.docstatus==0) { if(!frm.doc.__islocal && frm.perm[0].write && frm.doc.docstatus==0) {

View File

@ -34,11 +34,14 @@ def enroll_student(source_name):
} }
}}, ignore_permissions=True) }}, ignore_permissions=True)
student.save() student.save()
student_applicant = frappe.db.get_value("Student Applicant", source_name,
["student_category", "program"], as_dict=True)
program_enrollment = frappe.new_doc("Program Enrollment") program_enrollment = frappe.new_doc("Program Enrollment")
program_enrollment.student = student.name program_enrollment.student = student.name
program_enrollment.student_category = student.student_category program_enrollment.student_category = student_applicant.student_category
program_enrollment.student_name = student.title program_enrollment.student_name = student.title
program_enrollment.program = frappe.db.get_value("Student Applicant", source_name, "program") program_enrollment.program = student_applicant.program
frappe.publish_realtime('enroll_student_progress', {"progress": [2, 4]}, user=frappe.session.user) frappe.publish_realtime('enroll_student_progress', {"progress": [2, 4]}, user=frappe.session.user)
return program_enrollment return program_enrollment

View File

@ -1,195 +1,68 @@
{ {
"allow_copy": 0, "actions": [],
"allow_guest_to_view": 0, "creation": "2016-06-10 03:29:02.539914",
"allow_import": 0, "doctype": "DocType",
"allow_rename": 0, "editable_grid": 1,
"beta": 0, "engine": "InnoDB",
"creation": "2016-06-10 03:29:02.539914", "field_order": [
"custom": 0, "student_applicant",
"docstatus": 0, "student",
"doctype": "DocType", "student_name",
"document_type": "", "column_break_3",
"editable_grid": 1, "student_batch_name",
"student_category"
],
"fields": [ "fields": [
{ {
"allow_bulk_edit": 0, "fieldname": "student_applicant",
"allow_on_submit": 0, "fieldtype": "Link",
"bold": 0, "in_list_view": 1,
"collapsible": 0, "label": "Student Applicant",
"columns": 0, "options": "Student Applicant"
"depends_on": "", },
"fieldname": "student_applicant",
"fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Student Applicant",
"length": 0,
"no_copy": 0,
"options": "Student Applicant",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{ {
"allow_bulk_edit": 0, "fieldname": "student",
"allow_on_submit": 0, "fieldtype": "Link",
"bold": 0, "in_list_view": 1,
"collapsible": 0, "label": "Student",
"columns": 0, "options": "Student"
"depends_on": "", },
"fieldname": "student",
"fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Student",
"length": 0,
"no_copy": 0,
"options": "Student",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{ {
"allow_bulk_edit": 0, "fieldname": "column_break_3",
"allow_on_submit": 0, "fieldtype": "Column Break"
"bold": 0, },
"collapsible": 0,
"columns": 0,
"fieldname": "column_break_3",
"fieldtype": "Column Break",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{ {
"allow_bulk_edit": 0, "fieldname": "student_name",
"allow_on_submit": 0, "fieldtype": "Data",
"bold": 0, "in_list_view": 1,
"collapsible": 0, "label": "Student Name",
"columns": 0, "read_only": 1
"fieldname": "student_name", },
"fieldtype": "Data",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Student Name",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 1,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{ {
"allow_bulk_edit": 0, "fieldname": "student_batch_name",
"allow_on_submit": 0, "fieldtype": "Link",
"bold": 0, "in_list_view": 1,
"collapsible": 0, "label": "Student Batch Name",
"columns": 0, "options": "Student Batch Name"
"fieldname": "student_batch_name", },
"fieldtype": "Link", {
"hidden": 0, "fieldname": "student_category",
"ignore_user_permissions": 0, "fieldtype": "Link",
"ignore_xss_filter": 0, "label": "Student Category",
"in_filter": 0, "options": "Student Category",
"in_global_search": 0, "read_only": 1
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Student Batch Name",
"length": 0,
"no_copy": 0,
"options": "Student Batch Name",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
} }
], ],
"has_web_view": 0, "istable": 1,
"hide_heading": 0, "links": [],
"hide_toolbar": 0, "modified": "2021-07-29 18:19:54.471594",
"idx": 0, "modified_by": "Administrator",
"image_view": 0, "module": "Education",
"in_create": 0, "name": "Program Enrollment Tool Student",
"is_submittable": 0, "owner": "Administrator",
"issingle": 0, "permissions": [],
"istable": 1, "quick_entry": 1,
"max_attachments": 0, "restrict_to_domain": "Education",
"modified": "2018-01-02 12:03:53.890741", "sort_field": "modified",
"modified_by": "Administrator", "sort_order": "DESC"
"module": "Education",
"name": "Program Enrollment Tool Student",
"name_case": "",
"owner": "Administrator",
"permissions": [],
"quick_entry": 1,
"read_only": 0,
"read_only_onload": 0,
"restrict_to_domain": "Education",
"show_name_in_global_search": 0,
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 0,
"track_seen": 0
} }

View File

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

View File

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

View File

@ -430,7 +430,8 @@ regional_overrides = {
'erpnext.hr.utils.calculate_annual_eligible_hra_exemption': 'erpnext.regional.india.utils.calculate_annual_eligible_hra_exemption', 'erpnext.hr.utils.calculate_annual_eligible_hra_exemption': 'erpnext.regional.india.utils.calculate_annual_eligible_hra_exemption',
'erpnext.hr.utils.calculate_hra_exemption_for_period': 'erpnext.regional.india.utils.calculate_hra_exemption_for_period', 'erpnext.hr.utils.calculate_hra_exemption_for_period': 'erpnext.regional.india.utils.calculate_hra_exemption_for_period',
'erpnext.controllers.accounts_controller.validate_einvoice_fields': 'erpnext.regional.india.e_invoice.utils.validate_einvoice_fields', 'erpnext.controllers.accounts_controller.validate_einvoice_fields': 'erpnext.regional.india.e_invoice.utils.validate_einvoice_fields',
'erpnext.assets.doctype.asset.asset.get_depreciation_amount': 'erpnext.regional.india.utils.get_depreciation_amount' 'erpnext.assets.doctype.asset.asset.get_depreciation_amount': 'erpnext.regional.india.utils.get_depreciation_amount',
'erpnext.stock.doctype.item.item.set_item_tax_from_hsn_code': 'erpnext.regional.india.utils.set_item_tax_from_hsn_code'
}, },
'United Arab Emirates': { 'United Arab Emirates': {
'erpnext.controllers.taxes_and_totals.update_itemised_tax_data': 'erpnext.regional.united_arab_emirates.utils.update_itemised_tax_data', 'erpnext.controllers.taxes_and_totals.update_itemised_tax_data': 'erpnext.regional.united_arab_emirates.utils.update_itemised_tax_data',

View File

@ -24,7 +24,6 @@ class EmployeeAdvance(Document):
def on_cancel(self): def on_cancel(self):
self.ignore_linked_doctypes = ('GL Entry') self.ignore_linked_doctypes = ('GL Entry')
self.set_status()
def set_status(self): def set_status(self):
if self.docstatus == 0: if self.docstatus == 0:
@ -231,4 +230,4 @@ def get_voucher_type(mode_of_payment=None):
if mode_of_payment_type == "Bank": if mode_of_payment_type == "Bank":
voucher_type = "Bank Entry" voucher_type = "Bank Entry"
return voucher_type return voucher_type

View File

@ -36,8 +36,8 @@ class ExpenseClaim(AccountsController):
if self.task and not self.project: if self.task and not self.project:
self.project = frappe.db.get_value("Task", self.task, "project") self.project = frappe.db.get_value("Task", self.task, "project")
def set_status(self): def set_status(self, update=False):
self.status = { status = {
"0": "Draft", "0": "Draft",
"1": "Submitted", "1": "Submitted",
"2": "Cancelled" "2": "Cancelled"
@ -45,14 +45,18 @@ class ExpenseClaim(AccountsController):
paid_amount = flt(self.total_amount_reimbursed) + flt(self.total_advance_amount) paid_amount = flt(self.total_amount_reimbursed) + flt(self.total_advance_amount)
precision = self.precision("grand_total") precision = self.precision("grand_total")
if (self.is_paid or (flt(self.total_sanctioned_amount) > 0 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 flt(self.grand_total, precision) == flt(paid_amount, precision))) and self.approval_status == 'Approved':
and self.docstatus == 1 and self.approval_status == 'Approved': status = "Paid"
self.status = "Paid"
elif flt(self.total_sanctioned_amount) > 0 and self.docstatus == 1 and self.approval_status == 'Approved': elif flt(self.total_sanctioned_amount) > 0 and self.docstatus == 1 and self.approval_status == 'Approved':
self.status = "Unpaid" status = "Unpaid"
elif self.docstatus == 1 and self.approval_status == 'Rejected': elif self.docstatus == 1 and self.approval_status == 'Rejected':
self.status = 'Rejected' status = 'Rejected'
if update:
self.db_set("status", status)
else:
self.status = status
def on_update(self): def on_update(self):
share_doc_with_approver(self, self.expense_approver) share_doc_with_approver(self, self.expense_approver)
@ -75,7 +79,7 @@ class ExpenseClaim(AccountsController):
if self.is_paid: if self.is_paid:
update_reimbursed_amount(self) update_reimbursed_amount(self)
self.set_status() self.set_status(update=True)
self.update_claimed_amount_in_employee_advance() self.update_claimed_amount_in_employee_advance()
def on_cancel(self): def on_cancel(self):
@ -87,7 +91,6 @@ class ExpenseClaim(AccountsController):
if self.is_paid: if self.is_paid:
update_reimbursed_amount(self) update_reimbursed_amount(self)
self.set_status()
self.update_claimed_amount_in_employee_advance() self.update_claimed_amount_in_employee_advance()
def update_claimed_amount_in_employee_advance(self): def update_claimed_amount_in_employee_advance(self):

View File

@ -717,9 +717,8 @@ def get_bom_item_rate(args, bom_doc):
"ignore_conversion_rate": True "ignore_conversion_rate": True
}) })
item_doc = frappe.get_cached_doc("Item", args.get("item_code")) item_doc = frappe.get_cached_doc("Item", args.get("item_code"))
out = frappe._dict() price_list_data = get_price_list_rate(bom_args, item_doc)
get_price_list_rate(bom_args, item_doc, out) rate = price_list_data.price_list_rate
rate = out.price_list_rate
return rate return rate
@ -774,7 +773,7 @@ def get_bom_items_as_dict(bom, company, qty=1, fetch_exploded=1, fetch_scrap_ite
item.image, item.image,
bom.project, bom.project,
bom_item.rate, bom_item.rate,
bom_item.amount, sum(bom_item.{qty_field}/ifnull(bom.quantity, 1)) * bom_item.rate * %(qty)s as amount,
item.stock_uom, item.stock_uom,
item.item_group, item.item_group,
item.allow_alternative_item, item.allow_alternative_item,

View File

@ -608,6 +608,11 @@ def make_stock_entry(source_name, target_doc=None):
target.set_missing_values() target.set_missing_values()
target.set_stock_entry_type() target.set_stock_entry_type()
wo_allows_alternate_item = frappe.db.get_value("Work Order", target.work_order, "allow_alternative_item")
for item in target.items:
item.allow_alternative_item = int(wo_allows_alternate_item and
frappe.get_cached_value("Item", item.item_code, "allow_alternative_item"))
doclist = get_mapped_doc("Job Card", source_name, { doclist = get_mapped_doc("Job Card", source_name, {
"Job Card": { "Job Card": {
"doctype": "Stock Entry", "doctype": "Stock Entry",
@ -698,4 +703,4 @@ def make_corrective_job_card(source_name, operation=None, for_operation=None, ta
} }
}, target_doc, set_missing_values) }, target_doc, set_missing_values)
return doclist return doclist

View File

@ -109,6 +109,15 @@ class ProductionPlan(Document):
so_mr_list = [d.get(field) for d in self.get(table) if d.get(field)] so_mr_list = [d.get(field) for d in self.get(table) if d.get(field)]
return so_mr_list return so_mr_list
def get_bom_item(self):
"""Check if Item or if its Template has a BOM."""
bom_item = None
has_bom = frappe.db.exists({'doctype': 'BOM', 'item': self.item_code, 'docstatus': 1})
if not has_bom:
template_item = frappe.db.get_value('Item', self.item_code, ['variant_of'])
bom_item = "bom.item = {0}".format(frappe.db.escape(template_item)) if template_item else bom_item
return bom_item
def get_so_items(self): def get_so_items(self):
# Check for empty table or empty rows # Check for empty table or empty rows
if not self.get("sales_orders") or not self.get_so_mr_list("sales_order", "sales_orders"): if not self.get("sales_orders") or not self.get_so_mr_list("sales_order", "sales_orders"):
@ -117,16 +126,26 @@ class ProductionPlan(Document):
so_list = self.get_so_mr_list("sales_order", "sales_orders") so_list = self.get_so_mr_list("sales_order", "sales_orders")
item_condition = "" item_condition = ""
if self.item_code: bom_item = "bom.item = so_item.item_code"
if self.item_code and frappe.db.exists('Item', self.item_code):
bom_item = self.get_bom_item() or bom_item
item_condition = ' and so_item.item_code = {0}'.format(frappe.db.escape(self.item_code)) item_condition = ' and so_item.item_code = {0}'.format(frappe.db.escape(self.item_code))
items = frappe.db.sql("""select distinct parent, item_code, warehouse, items = frappe.db.sql("""
(qty - work_order_qty) * conversion_factor as pending_qty, description, name select
from `tabSales Order Item` so_item distinct parent, item_code, warehouse,
where parent in (%s) and docstatus = 1 and qty > work_order_qty (qty - work_order_qty) * conversion_factor as pending_qty,
and exists (select name from `tabBOM` bom where bom.item=so_item.item_code description, name
and bom.is_active = 1) %s""" % \ from
(", ".join(["%s"] * len(so_list)), item_condition), tuple(so_list), as_dict=1) `tabSales Order Item` so_item
where
parent in (%s) and docstatus = 1 and qty > work_order_qty
and exists (select name from `tabBOM` bom where %s
and bom.is_active = 1) %s""" %
(", ".join(["%s"] * len(so_list)),
bom_item,
item_condition),
tuple(so_list), as_dict=1)
if self.item_code: if self.item_code:
item_condition = ' and so_item.item_code = {0}'.format(frappe.db.escape(self.item_code)) item_condition = ' and so_item.item_code = {0}'.format(frappe.db.escape(self.item_code))
@ -683,6 +702,7 @@ def get_material_request_items(row, sales_order, company,
def get_sales_orders(self): def get_sales_orders(self):
so_filter = item_filter = "" so_filter = item_filter = ""
bom_item = "bom.item = so_item.item_code"
if self.from_date: if self.from_date:
so_filter += " and so.transaction_date >= %(from_date)s" so_filter += " and so.transaction_date >= %(from_date)s"
if self.to_date: if self.to_date:
@ -694,7 +714,8 @@ def get_sales_orders(self):
if self.sales_order_status: if self.sales_order_status:
so_filter += "and so.status = %(sales_order_status)s" so_filter += "and so.status = %(sales_order_status)s"
if self.item_code: if self.item_code and frappe.db.exists('Item', self.item_code):
bom_item = self.get_bom_item() or bom_item
item_filter += " and so_item.item_code = %(item)s" item_filter += " and so_item.item_code = %(item)s"
open_so = frappe.db.sql(""" open_so = frappe.db.sql("""
@ -704,13 +725,13 @@ def get_sales_orders(self):
and so.docstatus = 1 and so.status not in ("Stopped", "Closed") and so.docstatus = 1 and so.status not in ("Stopped", "Closed")
and so.company = %(company)s and so.company = %(company)s
and so_item.qty > so_item.work_order_qty {0} {1} and so_item.qty > so_item.work_order_qty {0} {1}
and (exists (select name from `tabBOM` bom where bom.item=so_item.item_code and (exists (select name from `tabBOM` bom where {2}
and bom.is_active = 1) and bom.is_active = 1)
or exists (select name from `tabPacked Item` pi or exists (select name from `tabPacked Item` pi
where pi.parent = so.name and pi.parent_item = so_item.item_code where pi.parent = so.name and pi.parent_item = so_item.item_code
and exists (select name from `tabBOM` bom where bom.item=pi.item_code and exists (select name from `tabBOM` bom where bom.item=pi.item_code
and bom.is_active = 1))) and bom.is_active = 1)))
""".format(so_filter, item_filter), { """.format(so_filter, item_filter, bom_item), {
"from_date": self.from_date, "from_date": self.from_date,
"to_date": self.to_date, "to_date": self.to_date,
"customer": self.customer, "customer": self.customer,

View File

@ -11,6 +11,7 @@ from erpnext.manufacturing.doctype.production_plan.production_plan import get_sa
from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation import create_stock_reconciliation from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation import create_stock_reconciliation
from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order
from erpnext.manufacturing.doctype.production_plan.production_plan import get_items_for_material_requests, get_warehouse_list from erpnext.manufacturing.doctype.production_plan.production_plan import get_items_for_material_requests, get_warehouse_list
from erpnext.controllers.item_variant import create_variant
class TestProductionPlan(unittest.TestCase): class TestProductionPlan(unittest.TestCase):
def setUp(self): def setUp(self):
@ -271,6 +272,60 @@ class TestProductionPlan(unittest.TestCase):
self.assertEqual(warehouses, expected_warehouses) self.assertEqual(warehouses, expected_warehouses)
def test_get_sales_order_with_variant(self):
if not frappe.db.exists('Item', {"item_code": 'PIV'}):
item = create_item('PIV', valuation_rate = 100)
variant_settings = {
"attributes": [
{
"attribute": "Colour"
},
],
"has_variants": 1
}
item.update(variant_settings)
item.save()
parent_bom = make_bom(item = 'PIV', raw_materials = ['PIV'])
if not frappe.db.exists('BOM', {"item": 'PIV'}):
parent_bom = make_bom(item = 'PIV', raw_materials = ['PIV'])
else:
parent_bom = frappe.get_doc('BOM', {"item": 'PIV'})
if not frappe.db.exists('Item', {"item_code": 'PIV-RED'}):
variant = create_variant("PIV", {"Colour": "Red"})
variant.save()
variant_bom = make_bom(item = variant.item_code, raw_materials = [variant.item_code])
else:
variant = frappe.get_doc('Item', 'PIV-RED')
if not frappe.db.exists('BOM', {"item": 'PIV-RED'}):
variant_bom = make_bom(item = variant.item_code, raw_materials = [variant.item_code])
"""Testing when item variant has a BOM"""
so = make_sales_order(item_code="PIV-RED", qty=5)
pln = frappe.new_doc('Production Plan')
pln.company = so.company
pln.get_items_from = 'Sales Order'
pln.item_code = 'PIV-RED'
pln.get_open_sales_orders()
self.assertEqual(pln.sales_orders[0].sales_order, so.name)
pln.get_so_items()
self.assertEqual(pln.po_items[0].item_code, 'PIV-RED')
self.assertEqual(pln.po_items[0].bom_no, variant_bom.name)
so.cancel()
frappe.delete_doc('Sales Order', so.name)
variant_bom.cancel()
frappe.delete_doc('BOM', variant_bom.name)
"""Testing when item variant doesn't have a BOM"""
so = make_sales_order(item_code="PIV-RED", qty=5)
pln.get_open_sales_orders()
self.assertEqual(pln.sales_orders[0].sales_order, so.name)
pln.po_items = []
pln.get_so_items()
self.assertEqual(pln.po_items[0].item_code, 'PIV-RED')
self.assertEqual(pln.po_items[0].bom_no, parent_bom.name)
frappe.db.rollback()
def create_production_plan(**args): def create_production_plan(**args):
args = frappe._dict(args) args = frappe._dict(args)

View File

@ -655,7 +655,7 @@ class WorkOrder(Document):
for item in sorted(item_dict.values(), key=lambda d: d['idx'] or 9999): for item in sorted(item_dict.values(), key=lambda d: d['idx'] or 9999):
self.append('required_items', { self.append('required_items', {
'rate': item.rate, 'rate': item.rate,
'amount': item.amount, 'amount': item.rate * item.qty,
'operation': item.operation or operation, 'operation': item.operation or operation,
'item_code': item.item_code, 'item_code': item.item_code,
'item_name': item.item_name, 'item_name': item.item_name,

View File

@ -293,5 +293,9 @@ erpnext.patches.v13_0.update_job_card_details
erpnext.patches.v13_0.update_level_in_bom #1234sswef erpnext.patches.v13_0.update_level_in_bom #1234sswef
erpnext.patches.v13_0.add_missing_fg_item_for_stock_entry erpnext.patches.v13_0.add_missing_fg_item_for_stock_entry
erpnext.patches.v13_0.update_subscription_status_in_memberships erpnext.patches.v13_0.update_subscription_status_in_memberships
erpnext.patches.v13_0.update_amt_in_work_order_required_items
erpnext.patches.v13_0.delete_orphaned_tables
erpnext.patches.v13_0.update_export_type_for_gst erpnext.patches.v13_0.update_export_type_for_gst
erpnext.patches.v13_0.update_tds_check_field #3 erpnext.patches.v13_0.update_tds_check_field #3
erpnext.patches.v13_0.update_recipient_email_digest
erpnext.patches.v13_0.shopify_deprecation_warning

View File

@ -30,19 +30,20 @@ def execute():
return return
repost_stock_entries = [] repost_stock_entries = []
stock_entries = frappe.db.sql_list(''' stock_entries = frappe.db.sql_list('''
SELECT SELECT
se.name se.name
FROM FROM
`tabStock Entry` se `tabStock Entry` se
WHERE WHERE
se.purpose = 'Manufacture' and se.docstatus < 2 and se.work_order in {work_orders} se.purpose = 'Manufacture' and se.docstatus < 2 and se.work_order in %s
and not exists( and not exists(
select name from `tabStock Entry Detail` sed where sed.parent = se.name and sed.is_finished_item = 1 select name from `tabStock Entry Detail` sed where sed.parent = se.name and sed.is_finished_item = 1
) )
Order BY ORDER BY
se.posting_date, se.posting_time se.posting_date, se.posting_time
'''.format(work_orders=tuple(work_orders))) ''', (work_orders,))
if stock_entries: if stock_entries:
print('Length of stock entries', len(stock_entries)) print('Length of stock entries', len(stock_entries))

View File

@ -0,0 +1,69 @@
# Copyright (c) 2019, Frappe and Contributors
# License: GNU General Public License v3. See license.txt
from __future__ import unicode_literals
import frappe
from frappe.utils import getdate
def execute():
frappe.reload_doc('setup', 'doctype', 'transaction_deletion_record')
if has_deleted_company_transactions():
child_doctypes = get_child_doctypes_whose_parent_doctypes_were_affected()
for doctype in child_doctypes:
docs = frappe.get_all(doctype, fields=['name', 'parent', 'parenttype', 'creation'])
for doc in docs:
if not frappe.db.exists(doc['parenttype'], doc['parent']):
frappe.db.delete(doctype, {'name': doc['name']})
elif check_for_new_doc_with_same_name_as_deleted_parent(doc):
frappe.db.delete(doctype, {'name': doc['name']})
def has_deleted_company_transactions():
return frappe.get_all('Transaction Deletion Record')
def get_child_doctypes_whose_parent_doctypes_were_affected():
parent_doctypes = get_affected_doctypes()
child_doctypes = frappe.get_all(
'DocField',
filters={
'fieldtype': 'Table',
'parent':['in', parent_doctypes]
}, pluck='options')
return child_doctypes
def get_affected_doctypes():
affected_doctypes = []
tdr_docs = frappe.get_all('Transaction Deletion Record', pluck="name")
for tdr in tdr_docs:
tdr_doc = frappe.get_doc("Transaction Deletion Record", tdr)
for doctype in tdr_doc.doctypes:
if is_not_child_table(doctype.doctype_name):
affected_doctypes.append(doctype.doctype_name)
affected_doctypes = remove_duplicate_items(affected_doctypes)
return affected_doctypes
def is_not_child_table(doctype):
return not bool(frappe.get_value('DocType', doctype, 'istable'))
def remove_duplicate_items(affected_doctypes):
return list(set(affected_doctypes))
def check_for_new_doc_with_same_name_as_deleted_parent(doc):
"""
Compares creation times of parent and child docs.
Since Transaction Deletion Record resets the naming series after deletion,
it allows the creation of new docs with the same names as the deleted ones.
"""
parent_creation_time = frappe.db.get_value(doc['parenttype'], doc['parent'], 'creation')
child_creation_time = doc['creation']
return getdate(parent_creation_time) > getdate(child_creation_time)

View File

@ -37,7 +37,7 @@ def execute():
if frappe.db.exists('DocType', 'Opportunity'): if frappe.db.exists('DocType', 'Opportunity'):
opportunities = frappe.db.get_all('Opportunity', fields=['name', 'mins_to_first_response'], order_by='creation desc') opportunities = frappe.db.get_all('Opportunity', fields=['name', 'mins_to_first_response'], order_by='creation desc')
frappe.reload_doc('crm', 'doctype', 'opportunity') frappe.reload_doctype('Opportunity', force=True)
rename_field('Opportunity', 'mins_to_first_response', 'first_response_time') rename_field('Opportunity', 'mins_to_first_response', 'first_response_time')
# change fieldtype to duration # change fieldtype to duration

View File

@ -0,0 +1,15 @@
import click
import frappe
def execute():
frappe.reload_doc("erpnext_integrations", "doctype", "shopify_settings")
if not frappe.db.get_single_value("Shopify Settings", "enable_shopify"):
return
click.secho(
"Shopify Integration is moved to a separate app and will be removed from ERPNext in version-14.\n"
"Please install the app to continue using the integration: https://github.com/frappe/ecommerce_integrations",
fg="yellow",
)

View File

@ -0,0 +1,10 @@
import frappe
def execute():
""" Correct amount in child table of required items table."""
frappe.reload_doc("manufacturing", "doctype", "work_order")
frappe.reload_doc("manufacturing", "doctype", "work_order_item")
frappe.db.sql("""UPDATE `tabWork Order Item` SET amount = rate * required_qty""")

View File

@ -0,0 +1,21 @@
# Copyright (c) 2020, Frappe and Contributors
# License: GNU General Public License v3. See license.txt
from __future__ import unicode_literals
import frappe
def execute():
frappe.reload_doc("setup", "doctype", "Email Digest")
frappe.reload_doc("setup", "doctype", "Email Digest Recipient")
email_digests = frappe.db.get_list('Email Digest', fields=['name', 'recipient_list'])
for email_digest in email_digests:
if email_digest.recipient_list:
for recipient in email_digest.recipient_list.split("\n"):
doc = frappe.get_doc({
'doctype': 'Email Digest Recipient',
'parenttype': 'Email Digest',
'parentfield': 'recipients',
'parent': email_digest.name,
'recipient': recipient
})
doc.insert()

View File

@ -112,11 +112,11 @@ class AdditionalSalary(Document):
no_of_days = date_diff(getdate(end_date), getdate(start_date)) + 1 no_of_days = date_diff(getdate(end_date), getdate(start_date)) + 1
return amount_per_day * no_of_days return amount_per_day * no_of_days
@frappe.whitelist()
def get_additional_salaries(employee, start_date, end_date, component_type): def get_additional_salaries(employee, start_date, end_date, component_type):
additional_salary_list = frappe.db.sql(""" additional_salary_list = frappe.db.sql("""
select name, salary_component as component, type, amount, overwrite_salary_structure_amount as overwrite, select name, salary_component as component, type, amount,
deduct_full_tax_on_selected_payroll_date, is_recurring overwrite_salary_structure_amount as overwrite,
deduct_full_tax_on_selected_payroll_date
from `tabAdditional Salary` from `tabAdditional Salary`
where employee=%(employee)s where employee=%(employee)s
and docstatus = 1 and docstatus = 1

View File

@ -4,11 +4,18 @@
frappe.ui.form.on('Salary Component', { frappe.ui.form.on('Salary Component', {
setup: function(frm) { setup: function(frm) {
frm.set_query("account", "accounts", function(doc, cdt, cdn) { frm.set_query("account", "accounts", function(doc, cdt, cdn) {
var d = locals[cdt][cdn]; let d = frappe.get_doc(cdt, cdn);
let root_type = "Liability";
if (frm.doc.type == "Deduction") {
root_type = "Expense";
}
return { return {
filters: { filters: {
"is_group": 0, "is_group": 0,
"company": d.company "company": d.company,
"root_type": root_type
} }
}; };
}); });

View File

@ -12,7 +12,6 @@
"year_to_date", "year_to_date",
"section_break_5", "section_break_5",
"additional_salary", "additional_salary",
"is_recurring_additional_salary",
"statistical_component", "statistical_component",
"depends_on_payment_days", "depends_on_payment_days",
"exempted_from_income_tax", "exempted_from_income_tax",
@ -236,19 +235,11 @@
"label": "Year To Date", "label": "Year To Date",
"options": "currency", "options": "currency",
"read_only": 1 "read_only": 1
},
{
"default": "0",
"depends_on": "eval:doc.parenttype=='Salary Slip' && doc.parentfield=='earnings' && doc.additional_salary",
"fieldname": "is_recurring_additional_salary",
"fieldtype": "Check",
"label": "Is Recurring Additional Salary",
"read_only": 1
} }
], ],
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2021-03-14 13:39:15.847158", "modified": "2021-01-14 13:39:15.847158",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Payroll", "module": "Payroll",
"name": "Salary Detail", "name": "Salary Detail",

View File

@ -7,12 +7,12 @@ import datetime, math
from frappe.utils import add_days, cint, cstr, flt, getdate, rounded, date_diff, money_in_words, formatdate, get_first_day from frappe.utils import add_days, cint, cstr, flt, getdate, rounded, date_diff, money_in_words, formatdate, get_first_day
from frappe.model.naming import make_autoname from frappe.model.naming import make_autoname
from frappe.utils.background_jobs import enqueue
from frappe import msgprint, _ from frappe import msgprint, _
from erpnext.payroll.doctype.payroll_entry.payroll_entry import get_start_end_dates from erpnext.payroll.doctype.payroll_entry.payroll_entry import get_start_end_dates
from erpnext.hr.doctype.employee.employee import get_holiday_list_for_employee from erpnext.hr.doctype.employee.employee import get_holiday_list_for_employee
from erpnext.utilities.transaction_base import TransactionBase from erpnext.utilities.transaction_base import TransactionBase
from frappe.utils.background_jobs import enqueue
from erpnext.payroll.doctype.additional_salary.additional_salary import get_additional_salaries from erpnext.payroll.doctype.additional_salary.additional_salary import get_additional_salaries
from erpnext.payroll.doctype.payroll_period.payroll_period import get_period_factor, get_payroll_period from erpnext.payroll.doctype.payroll_period.payroll_period import get_period_factor, get_payroll_period
from erpnext.payroll.doctype.employee_benefit_application.employee_benefit_application import get_benefit_component_amount from erpnext.payroll.doctype.employee_benefit_application.employee_benefit_application import get_benefit_component_amount
@ -618,8 +618,7 @@ class SalarySlip(TransactionBase):
get_salary_component_data(additional_salary.component), get_salary_component_data(additional_salary.component),
additional_salary.amount, additional_salary.amount,
component_type, component_type,
additional_salary, additional_salary
is_recurring = additional_salary.is_recurring
) )
def add_tax_components(self, payroll_period): def add_tax_components(self, payroll_period):
@ -640,7 +639,7 @@ class SalarySlip(TransactionBase):
tax_row = get_salary_component_data(d) tax_row = get_salary_component_data(d)
self.update_component_row(tax_row, tax_amount, "deductions") self.update_component_row(tax_row, tax_amount, "deductions")
def update_component_row(self, component_data, amount, component_type, additional_salary=None, is_recurring = 0): def update_component_row(self, component_data, amount, component_type, additional_salary=None):
component_row = None component_row = None
for d in self.get(component_type): for d in self.get(component_type):
if d.salary_component != component_data.salary_component: if d.salary_component != component_data.salary_component:
@ -681,7 +680,6 @@ class SalarySlip(TransactionBase):
component_row.set('abbr', abbr) component_row.set('abbr', abbr)
if additional_salary: if additional_salary:
component_row.is_recurring_additional_salary = is_recurring
component_row.default_amount = 0 component_row.default_amount = 0
component_row.additional_amount = amount component_row.additional_amount = amount
component_row.additional_salary = additional_salary.name component_row.additional_salary = additional_salary.name
@ -715,7 +713,6 @@ class SalarySlip(TransactionBase):
# get remaining numbers of sub-period (period for which one salary is processed) # get remaining numbers of sub-period (period for which one salary is processed)
remaining_sub_periods = get_period_factor(self.employee, remaining_sub_periods = get_period_factor(self.employee,
self.start_date, self.end_date, self.payroll_frequency, payroll_period)[1] self.start_date, self.end_date, self.payroll_frequency, payroll_period)[1]
# get taxable_earnings, paid_taxes for previous period # get taxable_earnings, paid_taxes for previous period
previous_taxable_earnings = self.get_taxable_earnings_for_prev_period(payroll_period.start_date, previous_taxable_earnings = self.get_taxable_earnings_for_prev_period(payroll_period.start_date,
self.start_date, tax_slab.allow_tax_exemption) self.start_date, tax_slab.allow_tax_exemption)
@ -875,16 +872,8 @@ class SalarySlip(TransactionBase):
if earning.is_tax_applicable: if earning.is_tax_applicable:
if additional_amount: if additional_amount:
if not earning.is_recurring_additional_salary: taxable_earnings += (amount - additional_amount)
taxable_earnings += (amount - additional_amount) additional_income += additional_amount
additional_income += additional_amount
else:
to_date = frappe.db.get_value("Additional Salary", earning.additional_salary, 'to_date')
period = (getdate(to_date).month - getdate(self.start_date).month) + 1
if period > 0:
taxable_earnings += (amount - additional_amount) * period
additional_income += additional_amount * period
if earning.deduct_full_tax_on_selected_payroll_date: if earning.deduct_full_tax_on_selected_payroll_date:
additional_income_with_full_tax += additional_amount additional_income_with_full_tax += additional_amount
continue continue

View File

@ -95,6 +95,7 @@ def execute(filters=None):
"amount": salary.net_pay, "amount": salary.net_pay,
} }
data.append(row) data.append(row)
return columns, data return columns, data
def get_bank_accounts(): def get_bank_accounts():
@ -116,7 +117,7 @@ def get_payroll_entries(accounts, filters):
entries = get_all("Payroll Entry", payroll_filter, ["name", "payment_account"]) entries = get_all("Payroll Entry", payroll_filter, ["name", "payment_account"])
payment_accounts = [d.payment_account for d in entries] payment_accounts = [d.payment_account for d in entries]
set_company_account(payment_accounts, entries) entries = set_company_account(payment_accounts, entries)
return entries return entries
def get_salary_slips(payroll_entries): def get_salary_slips(payroll_entries):

View File

@ -67,8 +67,6 @@ erpnext.taxes_and_totals = erpnext.payments.extend({
calculate_discount_amount: function() { calculate_discount_amount: function() {
if (frappe.meta.get_docfield(this.frm.doc.doctype, "discount_amount")) { if (frappe.meta.get_docfield(this.frm.doc.doctype, "discount_amount")) {
this.calculate_item_values();
this.calculate_net_total();
this.set_discount_amount(); this.set_discount_amount();
this.apply_discount_amount(); this.apply_discount_amount();
} }

View File

@ -0,0 +1,5 @@
{% if address_line1 %}{{ address_line1 }}{% endif -%}
{% if address_line2 %}<br>{{ address_line2 }}{% endif -%}
{% if pincode %}<br>{{ pincode }}{% endif -%}
{% if city %} {{ city }}{% endif -%}
{% if country %}<br>{{ country }}{% endif -%}

View File

@ -316,10 +316,6 @@ def get_payment_details(invoice):
)) ))
def get_return_doc_reference(invoice): def get_return_doc_reference(invoice):
if not invoice.return_against:
frappe.throw(_('For generating IRN, reference to the original invoice is mandatory for a credit note. Please set {} field to generate e-invoice.')
.format(frappe.bold('Return Against')), title=_('Missing Field'))
invoice_date = frappe.db.get_value('Sales Invoice', invoice.return_against, 'posting_date') invoice_date = frappe.db.get_value('Sales Invoice', invoice.return_against, 'posting_date')
return frappe._dict(dict( return frappe._dict(dict(
invoice_name=invoice.return_against, invoice_date=format_date(invoice_date, 'dd/mm/yyyy') invoice_name=invoice.return_against, invoice_date=format_date(invoice_date, 'dd/mm/yyyy')
@ -435,7 +431,7 @@ def make_einvoice(invoice):
if invoice.is_pos and invoice.base_paid_amount: if invoice.is_pos and invoice.base_paid_amount:
payment_details = get_payment_details(invoice) payment_details = get_payment_details(invoice)
if invoice.is_return: if invoice.is_return and invoice.return_against:
prev_doc_details = get_return_doc_reference(invoice) prev_doc_details = get_return_doc_reference(invoice)
if invoice.transporter and not invoice.is_return: if invoice.transporter and not invoice.is_return:
@ -966,7 +962,7 @@ class GSPConnector():
"attached_to_doctype": doctype, "attached_to_doctype": doctype,
"attached_to_name": docname, "attached_to_name": docname,
"attached_to_field": "qrcode_image", "attached_to_field": "qrcode_image",
"is_private": 1, "is_private": 0,
"content": qr_image.getvalue()}) "content": qr_image.getvalue()})
_file.save() _file.save()
frappe.db.commit() frappe.db.commit()

View File

@ -431,9 +431,11 @@ def get_ewb_data(dt, dn):
company_address = frappe.get_doc('Address', doc.company_address) company_address = frappe.get_doc('Address', doc.company_address)
billing_address = frappe.get_doc('Address', doc.customer_address) billing_address = frappe.get_doc('Address', doc.customer_address)
#added dispatch address
dispatch_address = frappe.get_doc('Address', doc.dispatch_address_name) if doc.dispatch_address_name else company_address
shipping_address = frappe.get_doc('Address', doc.shipping_address_name) shipping_address = frappe.get_doc('Address', doc.shipping_address_name)
data = get_address_details(data, doc, company_address, billing_address) data = get_address_details(data, doc, company_address, billing_address, dispatch_address)
data.itemList = [] data.itemList = []
data.totalValue = doc.total data.totalValue = doc.total
@ -519,10 +521,10 @@ def get_gstins_for_company(company):
`tabDynamic Link`.link_name = %(company)s""", {"company": company}) `tabDynamic Link`.link_name = %(company)s""", {"company": company})
return company_gstins return company_gstins
def get_address_details(data, doc, company_address, billing_address): def get_address_details(data, doc, company_address, billing_address, dispatch_address):
data.fromPincode = validate_pincode(company_address.pincode, 'Company Address') data.fromPincode = validate_pincode(company_address.pincode, 'Company Address')
data.fromStateCode = data.actualFromStateCode = validate_state_code( data.fromStateCode = validate_state_code(company_address.gst_state_number, 'Company Address')
company_address.gst_state_number, 'Company Address') data.actualFromStateCode = validate_state_code(dispatch_address.gst_state_number, 'Dispatch Address')
if not doc.billing_address_gstin or len(doc.billing_address_gstin) < 15: if not doc.billing_address_gstin or len(doc.billing_address_gstin) < 15:
data.toGstin = 'URP' data.toGstin = 'URP'
@ -834,8 +836,16 @@ def get_depreciation_amount(asset, depreciable_value, row):
depreciation_left = flt(row.total_number_of_depreciations) - flt(asset.number_of_depreciations_booked) depreciation_left = flt(row.total_number_of_depreciations) - flt(asset.number_of_depreciations_booked)
if row.depreciation_method in ("Straight Line", "Manual"): if row.depreciation_method in ("Straight Line", "Manual"):
depreciation_amount = (flt(row.value_after_depreciation) - # if the Depreciation Schedule is being prepared for the first time
flt(row.expected_value_after_useful_life)) / depreciation_left if not asset.flags.increase_in_asset_life:
depreciation_amount = (flt(row.value_after_depreciation) -
flt(row.expected_value_after_useful_life)) / depreciation_left
# if the Depreciation Schedule is being modified after Asset Repair
else:
depreciation_amount = (flt(row.value_after_depreciation) -
flt(row.expected_value_after_useful_life)) / (date_diff(asset.to_date, asset.available_for_use_date) / 365)
else: else:
rate_of_depreciation = row.rate_of_depreciation rate_of_depreciation = row.rate_of_depreciation
# if its the first depreciation # if its the first depreciation
@ -849,4 +859,15 @@ def get_depreciation_amount(asset, depreciable_value, row):
depreciation_amount = flt(depreciable_value * (flt(rate_of_depreciation) / 100)) depreciation_amount = flt(depreciable_value * (flt(rate_of_depreciation) / 100))
return depreciation_amount return depreciation_amount
def set_item_tax_from_hsn_code(item):
if not item.taxes and item.gst_hsn_code:
hsn_doc = frappe.get_doc("GST HSN Code", item.gst_hsn_code)
for tax in hsn_doc.taxes:
item.append('taxes', {
'item_tax_template': tax.item_tax_template,
'tax_category': tax.tax_category,
'valid_from': tax.valid_from
})

View File

@ -38,6 +38,8 @@
"col_break46", "col_break46",
"shipping_address_name", "shipping_address_name",
"shipping_address", "shipping_address",
"dispatch_address_name",
"dispatch_address",
"customer_group", "customer_group",
"territory", "territory",
"currency_and_price_list", "currency_and_price_list",
@ -1486,13 +1488,29 @@
"fieldname": "disable_rounded_total", "fieldname": "disable_rounded_total",
"fieldtype": "Check", "fieldtype": "Check",
"label": "Disable Rounded Total" "label": "Disable Rounded Total"
},
{
"allow_on_submit": 1,
"fieldname": "dispatch_address_name",
"fieldtype": "Link",
"label": "Dispatch Address Name",
"options": "Address",
"print_hide": 1
},
{
"allow_on_submit": 1,
"depends_on": "dispatch_address_name",
"fieldname": "dispatch_address",
"fieldtype": "Small Text",
"label": "Dispatch Address",
"read_only": 1
} }
], ],
"icon": "fa fa-file-text", "icon": "fa fa-file-text",
"idx": 105, "idx": 105,
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2021-04-15 23:55:13.439068", "modified": "2021-07-08 21:37:44.177493",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Selling", "module": "Selling",
"name": "Sales Order", "name": "Sales Order",

View File

@ -564,7 +564,6 @@ erpnext.PointOfSale.ItemCart = class {
) )
set_dynamic_rate_header_width(); set_dynamic_rate_header_width();
this.scroll_to_item($item_to_update);
function set_dynamic_rate_header_width() { function set_dynamic_rate_header_width() {
const rate_cols = Array.from(me.$cart_items_wrapper.find(".item-rate-amount")); const rate_cols = Array.from(me.$cart_items_wrapper.find(".item-rate-amount"));
@ -639,12 +638,6 @@ erpnext.PointOfSale.ItemCart = class {
$($img).parent().replaceWith(`<div class="item-image item-abbr">${item_abbr}</div>`); $($img).parent().replaceWith(`<div class="item-image item-abbr">${item_abbr}</div>`);
} }
scroll_to_item($item) {
if ($item.length === 0) return;
const scrollTop = $item.offset().top - this.$cart_items_wrapper.offset().top + this.$cart_items_wrapper.scrollTop();
this.$cart_items_wrapper.animate({ scrollTop });
}
update_selector_value_in_cart_item(selector, value, item) { update_selector_value_in_cart_item(selector, value, item) {
const $item_to_update = this.get_cart_item(item); const $item_to_update = this.get_cart_item(item);
$item_to_update.attr(`data-${selector}`, escape(value)); $item_to_update.attr(`data-${selector}`, escape(value));

View File

@ -198,6 +198,7 @@ erpnext.PointOfSale.Payment = class {
const is_cash_shortcuts_invisible = !this.$payment_modes.find('.cash-shortcuts').is(':visible'); const is_cash_shortcuts_invisible = !this.$payment_modes.find('.cash-shortcuts').is(':visible');
this.attach_cash_shortcuts(frm.doc); this.attach_cash_shortcuts(frm.doc);
!is_cash_shortcuts_invisible && this.$payment_modes.find('.cash-shortcuts').css('display', 'grid'); !is_cash_shortcuts_invisible && this.$payment_modes.find('.cash-shortcuts').css('display', 'grid');
this.render_payment_mode_dom();
}); });
frappe.ui.form.on('POS Invoice', 'loyalty_amount', (frm) => { frappe.ui.form.on('POS Invoice', 'loyalty_amount', (frm) => {

View File

@ -74,7 +74,7 @@
"stock_received_but_not_billed", "stock_received_but_not_billed",
"service_received_but_not_billed", "service_received_but_not_billed",
"expenses_included_in_valuation", "expenses_included_in_valuation",
"fixed_asset_depreciation_settings", "fixed_asset_defaults",
"accumulated_depreciation_account", "accumulated_depreciation_account",
"depreciation_expense_account", "depreciation_expense_account",
"series_for_depreciation_entry", "series_for_depreciation_entry",
@ -83,6 +83,7 @@
"disposal_account", "disposal_account",
"depreciation_cost_center", "depreciation_cost_center",
"capital_work_in_progress_account", "capital_work_in_progress_account",
"repair_and_maintenance_account",
"asset_received_but_not_billed", "asset_received_but_not_billed",
"budget_detail", "budget_detail",
"exception_budget_approver_role", "exception_budget_approver_role",
@ -519,12 +520,6 @@
"no_copy": 1, "no_copy": 1,
"options": "Account" "options": "Account"
}, },
{
"collapsible": 1,
"fieldname": "fixed_asset_depreciation_settings",
"fieldtype": "Section Break",
"label": "Fixed Asset Depreciation Settings"
},
{ {
"fieldname": "accumulated_depreciation_account", "fieldname": "accumulated_depreciation_account",
"fieldtype": "Link", "fieldtype": "Link",
@ -734,6 +729,18 @@
"fieldtype": "Link", "fieldtype": "Link",
"label": "Default Payment Discount Account", "label": "Default Payment Discount Account",
"options": "Account" "options": "Account"
},
{
"collapsible": 1,
"fieldname": "fixed_asset_defaults",
"fieldtype": "Section Break",
"label": "Fixed Asset Defaults"
},
{
"fieldname": "repair_and_maintenance_account",
"fieldtype": "Link",
"label": "Repair and Maintenance Account",
"options": "Account"
} }
], ],
"icon": "fa fa-building", "icon": "fa fa-building",
@ -741,7 +748,7 @@
"image_field": "company_logo", "image_field": "company_logo",
"is_tree": 1, "is_tree": 1,
"links": [], "links": [],
"modified": "2021-05-07 03:11:28.189740", "modified": "2021-05-12 16:51:08.187233",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Setup", "module": "Setup",
"name": "Company", "name": "Company",

View File

@ -1,78 +1,31 @@
// Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors // Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
// License: GNU General Public License v3. See license.txt // License: GNU General Public License v3. See license.txt
cur_frm.cscript.refresh = function(doc, dt, dn) { frappe.ui.form.on("Email Digest", {
doc = locals[dt][dn]; refresh: function(frm) {
cur_frm.add_custom_button(__('View Now'), function() { if (!frm.is_new()) {
frappe.call({ frm.add_custom_button(__('View Now'), function() {
method: 'erpnext.setup.doctype.email_digest.email_digest.get_digest_msg', frappe.call({
args: { method: 'erpnext.setup.doctype.email_digest.email_digest.get_digest_msg',
name: doc.name args: {
}, name: frm.doc.name
callback: function(r) { },
var d = new frappe.ui.Dialog({ callback: function(r) {
title: __('Email Digest: ') + dn, let d = new frappe.ui.Dialog({
width: 800 title: __('Email Digest: {0}', [frm.doc.name]),
width: 800
});
$(d.body).html(r.message);
d.show();
}
}); });
$(d.body).html(r.message);
d.show();
}
});
}, "fa fa-eye-open", "btn-default");
if (!cur_frm.is_new()) {
cur_frm.add_custom_button(__('Send Now'), function() {
return cur_frm.call('send', null, (r) => {
frappe.show_alert(__('Message Sent'));
}); });
});
frm.add_custom_button(__('Send Now'), function() {
return frm.call('send', null, () => {
frappe.show_alert({ message: __("Message Sent"), indicator: 'green'});
});
});
}
} }
}; });
cur_frm.cscript.addremove_recipients = function(doc, dt, dn) {
// Get user list
return cur_frm.call('get_users', null, function(r) {
// Open a dialog and display checkboxes against email addresses
doc = locals[dt][dn];
var d = new frappe.ui.Dialog({
title: __('Add/Remove Recipients'),
width: 400
});
$.each(r.user_list, function(i, v) {
var fullname = frappe.user.full_name(v.name);
if(fullname !== v.name) fullname = fullname + " &lt;" + v.name + "&gt;";
if(v.enabled==0) {
fullname = repl("<span style='color: red'> %(name)s (" + __("disabled user") + ")</span>", {name: v.name});
}
$('<div class="checkbox"><label>\
<input type="checkbox" data-id="' + v.name + '"'+
(v.checked ? 'checked' : '') +
'> '+ fullname +'</label></div>').appendTo(d.body);
});
// Display add recipients button
d.set_primary_action("Update", function() {
cur_frm.cscript.add_to_rec_list(doc, d.body, r.user_list.length);
});
cur_frm.rec_dialog = d;
d.show();
});
}
cur_frm.cscript.add_to_rec_list = function(doc, dialog, length) {
// add checked users to list of recipients
var rec_list = [];
$(dialog).find('input:checked').each(function(i, input) {
rec_list.push($(input).attr('data-id'));
});
doc.recipient_list = rec_list.join('\n');
cur_frm.rec_dialog.hide();
cur_frm.save();
cur_frm.refresh_fields();
}

File diff suppressed because it is too large Load Diff

View File

@ -47,19 +47,13 @@ class EmailDigest(Document):
# send email only to enabled users # send email only to enabled users
valid_users = [p[0] for p in frappe.db.sql("""select name from `tabUser` valid_users = [p[0] for p in frappe.db.sql("""select name from `tabUser`
where enabled=1""")] where enabled=1""")]
recipients = list(filter(lambda r: r in valid_users,
self.recipient_list.split("\n")))
original_user = frappe.session.user if self.recipients:
for row in self.recipients:
if recipients:
for user_id in recipients:
frappe.set_user(user_id)
frappe.set_user_lang(user_id)
msg_for_this_recipient = self.get_msg_html() msg_for_this_recipient = self.get_msg_html()
if msg_for_this_recipient: if msg_for_this_recipient and row.recipient in valid_users:
frappe.sendmail( frappe.sendmail(
recipients=user_id, recipients=row.recipient,
subject=_("{0} Digest").format(self.frequency), subject=_("{0} Digest").format(self.frequency),
message=msg_for_this_recipient, message=msg_for_this_recipient,
reference_doctype = self.doctype, reference_doctype = self.doctype,

View File

@ -0,0 +1,33 @@
{
"actions": [],
"creation": "2020-06-08 12:19:40.428949",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"recipient"
],
"fields": [
{
"fieldname": "recipient",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Recipient",
"options": "User",
"reqd": 1
}
],
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2020-08-24 23:10:23.217572",
"modified_by": "Administrator",
"module": "Setup",
"name": "Email Digest Recipient",
"owner": "Administrator",
"permissions": [],
"quick_entry": 1,
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
}

View File

@ -0,0 +1,10 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
from __future__ import unicode_literals
# import frappe
from frappe.model.document import Document
class EmailDigestRecipient(Document):
pass

View File

@ -54,7 +54,7 @@
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2021-05-08 23:13:48.049879", "modified": "2021-08-04 20:15:59.071493",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Setup", "module": "Setup",
"name": "Transaction Deletion Record", "name": "Transaction Deletion Record",
@ -70,6 +70,7 @@
"report": 1, "report": 1,
"role": "System Manager", "role": "System Manager",
"share": 1, "share": 1,
"submit": 1,
"write": 1 "write": 1
} }
], ],

View File

@ -45,9 +45,16 @@ def enable_shopping_cart(args):
def create_email_digest(): def create_email_digest():
from frappe.utils.user import get_system_managers from frappe.utils.user import get_system_managers
system_managers = get_system_managers(only_name=True) system_managers = get_system_managers(only_name=True)
if not system_managers: if not system_managers:
return return
recipients = []
for d in system_managers:
recipients.append({
'recipient': d
})
companies = frappe.db.sql_list("select name FROM `tabCompany`") companies = frappe.db.sql_list("select name FROM `tabCompany`")
for company in companies: for company in companies:
if not frappe.db.exists("Email Digest", "Default Weekly Digest - " + company): if not frappe.db.exists("Email Digest", "Default Weekly Digest - " + company):
@ -56,7 +63,7 @@ def create_email_digest():
"name": "Default Weekly Digest - " + company, "name": "Default Weekly Digest - " + company,
"company": company, "company": company,
"frequency": "Weekly", "frequency": "Weekly",
"recipient_list": "\n".join(system_managers) "recipients": recipients
}) })
for df in edigest.meta.get("fields", {"fieldtype": "Check"}): for df in edigest.meta.get("fields", {"fieldtype": "Check"}):
@ -72,7 +79,7 @@ def create_email_digest():
"name": "Scheduler Errors", "name": "Scheduler Errors",
"company": companies[0], "company": companies[0],
"frequency": "Daily", "frequency": "Daily",
"recipient_list": "\n".join(system_managers), "recipients": recipients,
"scheduler_errors": 1, "scheduler_errors": 1,
"enabled": 1 "enabled": 1
}) })

View File

@ -269,11 +269,14 @@ class TestBatch(unittest.TestCase):
batch2 = create_batch('_Test Batch Price Item', 300, 1) batch2 = create_batch('_Test Batch Price Item', 300, 1)
batch3 = create_batch('_Test Batch Price Item', 400, 0) batch3 = create_batch('_Test Batch Price Item', 400, 0)
company = "_Test Company with perpetual inventory"
currency = frappe.get_cached_value("Company", company, "default_currency")
args = frappe._dict({ args = frappe._dict({
"item_code": "_Test Batch Price Item", "item_code": "_Test Batch Price Item",
"company": "_Test Company with perpetual inventory", "company": company,
"price_list": "_Test Price List", "price_list": "_Test Price List",
"currency": "_Test Currency", "currency": currency,
"doctype": "Sales Invoice", "doctype": "Sales Invoice",
"conversion_rate": 1, "conversion_rate": 1,
"price_list_currency": "_Test Currency", "price_list_currency": "_Test Currency",
@ -333,4 +336,4 @@ def make_new_batch(**args):
except frappe.DuplicateEntryError: except frappe.DuplicateEntryError:
batch = frappe.get_doc("Batch", args.batch_id) batch = frappe.get_doc("Batch", args.batch_id)
return batch return batch

View File

@ -32,6 +32,8 @@
"contact_info", "contact_info",
"shipping_address_name", "shipping_address_name",
"shipping_address", "shipping_address",
"dispatch_address_name",
"dispatch_address",
"contact_person", "contact_person",
"contact_display", "contact_display",
"contact_mobile", "contact_mobile",
@ -1282,13 +1284,28 @@
"fieldname": "disable_rounded_total", "fieldname": "disable_rounded_total",
"fieldtype": "Check", "fieldtype": "Check",
"label": "Disable Rounded Total" "label": "Disable Rounded Total"
},
{
"fieldname": "dispatch_address_name",
"fieldtype": "Link",
"label": "Dispatch Address Name",
"options": "Address",
"print_hide": 1
},
{
"depends_on": "dispatch_address_name",
"fieldname": "dispatch_address",
"fieldtype": "Small Text",
"label": "Dispatch Address",
"print_hide": 1,
"read_only": 1
} }
], ],
"icon": "fa fa-truck", "icon": "fa fa-truck",
"idx": 146, "idx": 146,
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2021-06-11 19:27:30.901112", "modified": "2021-07-08 21:37:20.802652",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Stock", "module": "Stock",
"name": "Delivery Note", "name": "Delivery Note",

View File

@ -138,20 +138,6 @@ frappe.ui.form.on("Item", {
frm.toggle_reqd('customer', frm.doc.is_customer_provided_item ? 1:0); frm.toggle_reqd('customer', frm.doc.is_customer_provided_item ? 1:0);
}, },
gst_hsn_code: function(frm) {
if((!frm.doc.taxes || !frm.doc.taxes.length) && frm.doc.gst_hsn_code) {
frappe.db.get_doc("GST HSN Code", frm.doc.gst_hsn_code).then(hsn_doc => {
$.each(hsn_doc.taxes || [], function(i, tax) {
let a = frappe.model.add_child(cur_frm.doc, 'Item Tax', 'taxes');
a.item_tax_template = tax.item_tax_template;
a.tax_category = tax.tax_category;
a.valid_from = tax.valid_from;
frm.refresh_field('taxes');
});
});
}
},
is_fixed_asset: function(frm) { is_fixed_asset: function(frm) {
// set serial no to false & toggles its visibility // set serial no to false & toggles its visibility
frm.set_value('has_serial_no', 0); frm.set_value('has_serial_no', 0);

View File

@ -123,6 +123,7 @@ class Item(WebsiteGenerator):
self.cant_change() self.cant_change()
self.update_show_in_website() self.update_show_in_website()
self.validate_item_tax_net_rate_range() self.validate_item_tax_net_rate_range()
set_item_tax_from_hsn_code(self)
if not self.is_new(): if not self.is_new():
self.old_item_group = frappe.db.get_value(self.doctype, self.name, "item_group") self.old_item_group = frappe.db.get_value(self.doctype, self.name, "item_group")
@ -1305,3 +1306,7 @@ def update_variants(variants, template, publish_progress=True):
def on_doctype_update(): def on_doctype_update():
# since route is a Text column, it needs a length for indexing # since route is a Text column, it needs a length for indexing
frappe.db.add_index("Item", ["route(500)"]) frappe.db.add_index("Item", ["route(500)"])
@erpnext.allow_regional
def set_item_tax_from_hsn_code(item):
pass

View File

@ -0,0 +1,15 @@
frappe.ui.form.on('Item', {
gst_hsn_code: function(frm) {
if ((!frm.doc.taxes || !frm.doc.taxes.length) && frm.doc.gst_hsn_code) {
frappe.db.get_doc("GST HSN Code", frm.doc.gst_hsn_code).then(hsn_doc => {
$.each(hsn_doc.taxes || [], function(i, tax) {
let a = frappe.model.add_child(cur_frm.doc, 'Item Tax', 'taxes');
a.item_tax_template = tax.item_tax_template;
a.tax_category = tax.tax_category;
a.valid_from = tax.valid_from;
frm.refresh_field('taxes');
});
});
}
},
});

View File

@ -83,14 +83,17 @@ class TestItem(unittest.TestCase):
make_test_objects("Item Price") make_test_objects("Item Price")
company = "_Test Company"
currency = frappe.get_cached_value("Company", company, "default_currency")
details = get_item_details({ details = get_item_details({
"item_code": "_Test Item", "item_code": "_Test Item",
"company": "_Test Company", "company": company,
"price_list": "_Test Price List", "price_list": "_Test Price List",
"currency": "_Test Currency", "currency": currency,
"doctype": "Sales Order", "doctype": "Sales Order",
"conversion_rate": 1, "conversion_rate": 1,
"price_list_currency": "_Test Currency", "price_list_currency": currency,
"plc_conversion_rate": 1, "plc_conversion_rate": 1,
"order_type": "Sales", "order_type": "Sales",
"customer": "_Test Customer", "customer": "_Test Customer",

View File

@ -47,7 +47,8 @@
"description": "Simple Python formula applied on Reading fields.<br> Numeric eg. 1: <b>reading_1 &gt; 0.2 and reading_1 &lt; 0.5</b><br>\nNumeric eg. 2: <b>mean &gt; 3.5</b> (mean of populated fields)<br>\nValue based eg.: <b>reading_value in (\"A\", \"B\", \"C\")</b>", "description": "Simple Python formula applied on Reading fields.<br> Numeric eg. 1: <b>reading_1 &gt; 0.2 and reading_1 &lt; 0.5</b><br>\nNumeric eg. 2: <b>mean &gt; 3.5</b> (mean of populated fields)<br>\nValue based eg.: <b>reading_value in (\"A\", \"B\", \"C\")</b>",
"fieldname": "acceptance_formula", "fieldname": "acceptance_formula",
"fieldtype": "Code", "fieldtype": "Code",
"label": "Acceptance Criteria Formula" "label": "Acceptance Criteria Formula",
"options": "PythonExpression"
}, },
{ {
"default": "0", "default": "0",
@ -89,7 +90,7 @@
"idx": 1, "idx": 1,
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2021-02-04 18:50:02.056173", "modified": "2021-08-06 15:08:20.911338",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Stock", "module": "Stock",
"name": "Item Quality Inspection Parameter", "name": "Item Quality Inspection Parameter",

View File

@ -23,9 +23,7 @@ class TestPurchaseReceipt(unittest.TestCase):
def test_reverse_purchase_receipt_sle(self): def test_reverse_purchase_receipt_sle(self):
frappe.db.set_value('UOM', '_Test UOM', 'must_be_whole_number', 0) pr = make_purchase_receipt(qty=0.5, item_code="_Test Item Home Desktop 200")
pr = make_purchase_receipt(qty=0.5)
sl_entry = frappe.db.get_all("Stock Ledger Entry", {"voucher_type": "Purchase Receipt", sl_entry = frappe.db.get_all("Stock Ledger Entry", {"voucher_type": "Purchase Receipt",
"voucher_no": pr.name}, ['actual_qty']) "voucher_no": pr.name}, ['actual_qty'])
@ -41,8 +39,6 @@ class TestPurchaseReceipt(unittest.TestCase):
self.assertEqual(len(sl_entry_cancelled), 2) self.assertEqual(len(sl_entry_cancelled), 2)
self.assertEqual(sl_entry_cancelled[1].actual_qty, -0.5) self.assertEqual(sl_entry_cancelled[1].actual_qty, -0.5)
frappe.db.set_value('UOM', '_Test UOM', 'must_be_whole_number', 1)
def test_make_purchase_invoice(self): def test_make_purchase_invoice(self):
if not frappe.db.exists('Payment Terms Template', '_Test Payment Terms Template For Purchase Invoice'): if not frappe.db.exists('Payment Terms Template', '_Test Payment Terms Template For Purchase Invoice'):
frappe.get_doc({ frappe.get_doc({
@ -328,18 +324,7 @@ class TestPurchaseReceipt(unittest.TestCase):
pr1.submit() pr1.submit()
self.assertRaises(frappe.ValidationError, pr2.submit) self.assertRaises(frappe.ValidationError, pr2.submit)
frappe.db.rollback()
pr1.cancel()
se.cancel()
se1.cancel()
se2.cancel()
se3.cancel()
po.reload()
pr2.load_from_db()
pr2.cancel()
po.load_from_db()
po.cancel()
def test_serial_no_supplier(self): def test_serial_no_supplier(self):
pr = make_purchase_receipt(item_code="_Test Serialized Item With Series", qty=1) pr = make_purchase_receipt(item_code="_Test Serialized Item With Series", qty=1)
@ -1044,7 +1029,7 @@ class TestPurchaseReceipt(unittest.TestCase):
'account': srbnb_account, 'account': srbnb_account,
'voucher_detail_no': pr.items[1].name 'voucher_detail_no': pr.items[1].name
}, pluck="name") }, pluck="name")
# check if the entries are not merged into one # check if the entries are not merged into one
# seperate entries should be made since voucher_detail_no is different # seperate entries should be made since voucher_detail_no is different
self.assertEqual(len(item_one_gl_entry), 1) self.assertEqual(len(item_one_gl_entry), 1)

View File

@ -29,13 +29,50 @@ frappe.ui.form.on('Repost Item Valuation', {
}; };
}); });
} }
frm.trigger('setup_realtime_progress');
}, },
setup_realtime_progress: function(frm) {
frappe.realtime.on('item_reposting_progress', data => {
if (frm.doc.name !== data.name) {
return;
}
if (frm.doc.status == 'In Progress') {
frm.doc.current_index = data.current_index;
frm.doc.items_to_be_repost = data.items_to_be_repost;
frm.dashboard.reset();
frm.trigger('show_reposting_progress');
}
});
},
refresh: function(frm) { refresh: function(frm) {
if (frm.doc.status == "Failed" && frm.doc.docstatus==1) { if (frm.doc.status == "Failed" && frm.doc.docstatus==1) {
frm.add_custom_button(__('Restart'), function () { frm.add_custom_button(__('Restart'), function () {
frm.trigger("restart_reposting"); frm.trigger("restart_reposting");
}).addClass("btn-primary"); }).addClass("btn-primary");
} }
frm.trigger('show_reposting_progress');
},
show_reposting_progress: function(frm) {
var bars = [];
let total_count = frm.doc.items_to_be_repost ? JSON.parse(frm.doc.items_to_be_repost).length : 0;
let progress = flt(cint(frm.doc.current_index) / total_count * 100, 2) || 0.5;
var title = __('Reposting Completed {0}%', [progress]);
bars.push({
'title': title,
'width': progress + '%',
'progress_class': 'progress-bar-success'
});
frm.dashboard.add_progress(__('Reposting Progress'), bars);
}, },
restart_reposting: function(frm) { restart_reposting: function(frm) {

View File

@ -21,7 +21,10 @@
"allow_zero_rate", "allow_zero_rate",
"amended_from", "amended_from",
"error_section", "error_section",
"error_log" "error_log",
"items_to_be_repost",
"distinct_item_and_warehouse",
"current_index"
], ],
"fields": [ "fields": [
{ {
@ -142,12 +145,39 @@
"fieldname": "allow_zero_rate", "fieldname": "allow_zero_rate",
"fieldtype": "Check", "fieldtype": "Check",
"label": "Allow Zero Rate" "label": "Allow Zero Rate"
},
{
"fieldname": "items_to_be_repost",
"fieldtype": "Code",
"hidden": 1,
"label": "Items to Be Repost",
"no_copy": 1,
"print_hide": 1,
"read_only": 1
},
{
"fieldname": "distinct_item_and_warehouse",
"fieldtype": "Code",
"hidden": 1,
"label": "Distinct Item and Warehouse",
"no_copy": 1,
"print_hide": 1,
"read_only": 1
},
{
"fieldname": "current_index",
"fieldtype": "Int",
"hidden": 1,
"label": "Current Index",
"no_copy": 1,
"print_hide": 1,
"read_only": 1
} }
], ],
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2020-12-10 07:52:12.476589", "modified": "2021-07-22 18:59:43.057878",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Stock", "module": "Stock",
"name": "Repost Item Valuation", "name": "Repost Item Valuation",

View File

@ -80,7 +80,7 @@ def repost(doc):
def repost_sl_entries(doc): def repost_sl_entries(doc):
if doc.based_on == 'Transaction': if doc.based_on == 'Transaction':
repost_future_sle(voucher_type=doc.voucher_type, voucher_no=doc.voucher_no, repost_future_sle(doc=doc, voucher_type=doc.voucher_type, voucher_no=doc.voucher_no,
allow_negative_stock=doc.allow_negative_stock, via_landed_cost_voucher=doc.via_landed_cost_voucher) allow_negative_stock=doc.allow_negative_stock, via_landed_cost_voucher=doc.via_landed_cost_voucher)
else: else:
repost_future_sle(args=[frappe._dict({ repost_future_sle(args=[frappe._dict({

View File

@ -74,8 +74,7 @@ def get_item_details(args, doc=None, for_validate=False, overwrite_warehouse=Tru
update_party_blanket_order(args, out) update_party_blanket_order(args, out)
out.update(get_price_list_rate(args, item))
get_price_list_rate(args, item, out)
if args.customer and cint(args.is_pos): if args.customer and cint(args.is_pos):
out.update(get_pos_profile_item_details(args.company, args, update_data=True)) out.update(get_pos_profile_item_details(args.company, args, update_data=True))
@ -312,8 +311,8 @@ def get_basic_details(args, item, overwrite_warehouse=True):
"transaction_date": args.get("transaction_date"), "transaction_date": args.get("transaction_date"),
"against_blanket_order": args.get("against_blanket_order"), "against_blanket_order": args.get("against_blanket_order"),
"bom_no": item.get("default_bom"), "bom_no": item.get("default_bom"),
"weight_per_unit": args.get("weight_per_unit") or item.get("weight_per_unit"), "weight_per_unit": item.get("weight_per_unit"),
"weight_uom": args.get("weight_uom") or item.get("weight_uom") "weight_uom": item.get("weight_uom")
}) })
if item.get("enable_deferred_revenue") or item.get("enable_deferred_expense"): if item.get("enable_deferred_revenue") or item.get("enable_deferred_expense"):
@ -638,7 +637,10 @@ def get_default_supplier(args, item, item_group, brand):
or item_group.get("default_supplier") or item_group.get("default_supplier")
or brand.get("default_supplier")) or brand.get("default_supplier"))
def get_price_list_rate(args, item_doc, out): def get_price_list_rate(args, item_doc, out=None):
if out is None:
out = frappe._dict()
meta = frappe.get_meta(args.parenttype or args.doctype) meta = frappe.get_meta(args.parenttype or args.doctype)
if meta.get_field("currency") or args.get('currency'): if meta.get_field("currency") or args.get('currency'):
@ -651,17 +653,17 @@ def get_price_list_rate(args, item_doc, out):
if meta.get_field("currency"): if meta.get_field("currency"):
validate_conversion_rate(args, meta) validate_conversion_rate(args, meta)
price_list_rate = get_price_list_rate_for(args, item_doc.name) or 0 price_list_rate = get_price_list_rate_for(args, item_doc.name)
# variant # variant
if not price_list_rate and item_doc.variant_of: if price_list_rate is None and item_doc.variant_of:
price_list_rate = get_price_list_rate_for(args, item_doc.variant_of) price_list_rate = get_price_list_rate_for(args, item_doc.variant_of)
# insert in database # insert in database
if not price_list_rate: if price_list_rate is None:
if args.price_list and args.rate: if args.price_list and args.rate:
insert_item_price(args) insert_item_price(args)
return {} return out
out.price_list_rate = flt(price_list_rate) * flt(args.plc_conversion_rate) \ out.price_list_rate = flt(price_list_rate) * flt(args.plc_conversion_rate) \
/ flt(args.conversion_rate) / flt(args.conversion_rate)
@ -671,6 +673,8 @@ def get_price_list_rate(args, item_doc, out):
out.update(get_last_purchase_details(item_doc.name, out.update(get_last_purchase_details(item_doc.name,
args.name, args.conversion_rate)) args.name, args.conversion_rate))
return out
def insert_item_price(args): def insert_item_price(args):
"""Insert Item Price if Price List and Price List Rate are specified and currency is the same""" """Insert Item Price if Price List and Price List Rate are specified and currency is the same"""
if frappe.db.get_value("Price List", args.price_list, "currency", cache=True) == args.currency \ if frappe.db.get_value("Price List", args.price_list, "currency", cache=True) == args.currency \
@ -1073,9 +1077,8 @@ def apply_price_list(args, as_doc=False):
} }
def apply_price_list_on_item(args): def apply_price_list_on_item(args):
item_details = frappe._dict()
item_doc = frappe.get_doc("Item", args.item_code) item_doc = frappe.get_doc("Item", args.item_code)
get_price_list_rate(args, item_doc, item_details) item_details = get_price_list_rate(args, item_doc)
item_details.update(get_pricing_rule_for_item(args, item_details.price_list_rate)) item_details.update(get_pricing_rule_for_item(args, item_details.price_list_rate))

View File

@ -16,8 +16,6 @@ def execute(filters=None):
is_reposting_item_valuation_in_progress() is_reposting_item_valuation_in_progress()
if not filters: filters = {} if not filters: filters = {}
validate_filters(filters)
from_date = filters.get('from_date') from_date = filters.get('from_date')
to_date = filters.get('to_date') to_date = filters.get('to_date')
@ -295,12 +293,6 @@ def get_item_reorder_details(items):
return dict((d.parent + d.warehouse, d) for d in item_reorder_details) return dict((d.parent + d.warehouse, d) for d in item_reorder_details)
def validate_filters(filters):
if not (filters.get("item_code") or filters.get("warehouse")):
sle_count = flt(frappe.db.sql("""select count(name) from `tabStock Ledger Entry`""")[0][0])
if sle_count > 500000:
frappe.throw(_("Please set filter based on Item or Warehouse due to a large amount of entries."))
def get_variants_attributes(): def get_variants_attributes():
'''Return all item variant attributes.''' '''Return all item variant attributes.'''
return [i.name for i in frappe.get_all('Item Attribute')] return [i.name for i in frappe.get_all('Item Attribute')]

View File

@ -127,30 +127,24 @@ def make_entry(args, allow_negative_stock=False, via_landed_cost_voucher=False):
sle.submit() sle.submit()
return sle return sle
def repost_future_sle(args=None, voucher_type=None, voucher_no=None, allow_negative_stock=None, via_landed_cost_voucher=False): def repost_future_sle(args=None, doc=None, voucher_type=None, voucher_no=None, allow_negative_stock=None, via_landed_cost_voucher=False):
if not args and voucher_type and voucher_no: if not args and voucher_type and voucher_no:
args = get_args_for_voucher(voucher_type, voucher_no) args = get_items_to_be_repost(voucher_type, voucher_no, doc)
distinct_item_warehouses = {} distinct_item_warehouses = get_distinct_item_warehouse(args, doc)
for i, d in enumerate(args):
distinct_item_warehouses.setdefault((d.item_code, d.warehouse), frappe._dict({
"reposting_status": False,
"sle": d,
"args_idx": i
}))
i = 0 i = get_current_index(doc) or 0
while i < len(args): while i < len(args):
obj = update_entries_after({ obj = update_entries_after({
"item_code": args[i].item_code, "item_code": args[i].get('item_code'),
"warehouse": args[i].warehouse, "warehouse": args[i].get('warehouse'),
"posting_date": args[i].posting_date, "posting_date": args[i].get('posting_date'),
"posting_time": args[i].posting_time, "posting_time": args[i].get('posting_time'),
"creation": args[i].get("creation"), "creation": args[i].get("creation"),
"distinct_item_warehouses": distinct_item_warehouses "distinct_item_warehouses": distinct_item_warehouses
}, allow_negative_stock=allow_negative_stock, via_landed_cost_voucher=via_landed_cost_voucher) }, allow_negative_stock=allow_negative_stock, via_landed_cost_voucher=via_landed_cost_voucher)
distinct_item_warehouses[(args[i].item_code, args[i].warehouse)].reposting_status = True distinct_item_warehouses[(args[i].get('item_code'), args[i].get('warehouse'))].reposting_status = True
if obj.new_items_found: if obj.new_items_found:
for item_wh, data in iteritems(distinct_item_warehouses): for item_wh, data in iteritems(distinct_item_warehouses):
@ -159,11 +153,35 @@ def repost_future_sle(args=None, voucher_type=None, voucher_no=None, allow_negat
args.append(data.sle) args.append(data.sle)
elif data.sle_changed and not data.reposting_status: elif data.sle_changed and not data.reposting_status:
args[data.args_idx] = data.sle args[data.args_idx] = data.sle
data.sle_changed = False data.sle_changed = False
i += 1 i += 1
def get_args_for_voucher(voucher_type, voucher_no): if doc and i % 2 == 0:
update_args_in_repost_item_valuation(doc, i, args, distinct_item_warehouses)
if doc and args:
update_args_in_repost_item_valuation(doc, i, args, distinct_item_warehouses)
def update_args_in_repost_item_valuation(doc, index, args, distinct_item_warehouses):
frappe.db.set_value(doc.doctype, doc.name, {
'items_to_be_repost': json.dumps(args, default=str),
'distinct_item_and_warehouse': json.dumps({str(k): v for k,v in distinct_item_warehouses.items()}, default=str),
'current_index': index
})
frappe.db.commit()
frappe.publish_realtime('item_reposting_progress', {
'name': doc.name,
'items_to_be_repost': json.dumps(args, default=str),
'current_index': index
})
def get_items_to_be_repost(voucher_type, voucher_no, doc=None):
if doc and doc.items_to_be_repost:
return json.loads(doc.items_to_be_repost) or []
return frappe.db.get_all("Stock Ledger Entry", return frappe.db.get_all("Stock Ledger Entry",
filters={"voucher_type": voucher_type, "voucher_no": voucher_no}, filters={"voucher_type": voucher_type, "voucher_no": voucher_no},
fields=["item_code", "warehouse", "posting_date", "posting_time", "creation"], fields=["item_code", "warehouse", "posting_date", "posting_time", "creation"],
@ -171,6 +189,25 @@ def get_args_for_voucher(voucher_type, voucher_no):
group_by="item_code, warehouse" group_by="item_code, warehouse"
) )
def get_distinct_item_warehouse(args=None, doc=None):
distinct_item_warehouses = {}
if doc and doc.distinct_item_and_warehouse:
distinct_item_warehouses = json.loads(doc.distinct_item_and_warehouse)
distinct_item_warehouses = {frappe.safe_eval(k): frappe._dict(v) for k, v in distinct_item_warehouses.items()}
else:
for i, d in enumerate(args):
distinct_item_warehouses.setdefault((d.item_code, d.warehouse), frappe._dict({
"reposting_status": False,
"sle": d,
"args_idx": i
}))
return distinct_item_warehouses
def get_current_index(doc=None):
if doc and doc.current_index:
return doc.current_index
class update_entries_after(object): class update_entries_after(object):
""" """
update valution rate and qty after transaction update valution rate and qty after transaction

View File

@ -142,7 +142,7 @@ def link_existing_conversations(doc, state):
for log in logs: for log in logs:
call_log = frappe.get_doc('Call Log', log) call_log = frappe.get_doc('Call Log', log)
call_log.add_link(link_type=doc.doctype, link_name=doc.name) call_log.add_link(link_type=doc.doctype, link_name=doc.name)
call_log.save() call_log.save(ignore_permissions=True)
frappe.db.commit() frappe.db.commit()
except Exception: except Exception:
frappe.log_error(title=_('Error during caller information update')) frappe.log_error(title=_('Error during caller information update'))