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

This commit is contained in:
Deepesh Garg 2021-06-10 12:04:38 +05:30
commit 701cba7f03
123 changed files with 4081 additions and 4535 deletions

12
.git-blame-ignore-revs Normal file
View File

@ -0,0 +1,12 @@
# Since version 2.23 (released in August 2019), git-blame has a feature
# to ignore or bypass certain commits.
#
# This file contains a list of commits that are not likely what you
# are looking for in a blame, such as mass reformatting or renaming.
# You can set this file as a default ignore file for blame by running
# the following command.
#
# $ git config blame.ignoreRevsFile .git-blame-ignore-revs
# This commit just changes spaces to tabs for indentation in some files
5f473611bd6ed57703716244a054d3fb5ba9cd23

69
.github/workflows/patch.yml vendored Normal file
View File

@ -0,0 +1,69 @@
name: Patch
on: [pull_request, workflow_dispatch]
jobs:
test:
runs-on: ubuntu-18.04
name: Patch Test
services:
mysql:
image: mariadb:10.3
env:
MYSQL_ALLOW_EMPTY_PASSWORD: YES
ports:
- 3306:3306
options: --health-cmd="mysqladmin ping" --health-interval=5s --health-timeout=2s --health-retries=3
steps:
- name: Clone
uses: actions/checkout@v2
- name: Setup Python
uses: actions/setup-python@v2
with:
python-version: 3.6
- name: Add to Hosts
run: echo "127.0.0.1 test_site" | sudo tee -a /etc/hosts
- name: Cache pip
uses: actions/cache@v2
with:
path: ~/.cache/pip
key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }}
restore-keys: |
${{ runner.os }}-pip-
${{ runner.os }}-
- name: Cache node modules
uses: actions/cache@v2
env:
cache-name: cache-node-modules
with:
path: ~/.npm
key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-build-${{ env.cache-name }}-
${{ runner.os }}-build-
${{ runner.os }}-
- name: Get yarn cache directory path
id: yarn-cache-dir-path
run: echo "::set-output name=dir::$(yarn cache dir)"
- uses: actions/cache@v2
id: yarn-cache
with:
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
restore-keys: |
${{ runner.os }}-yarn-
- name: Install
run: bash ${GITHUB_WORKSPACE}/.github/helper/install.sh
- name: Run Patch Tests
run: cd ~/frappe-bench/ && wget http://build.erpnext.com/20171108_190013_955977f8_database.sql.gz && bench --site test_site --force restore ~/frappe-bench/20171108_190013_955977f8_database.sql.gz && bench --site test_site migrate

View File

@ -1,6 +1,6 @@
name: CI name: Server
on: [pull_request, workflow_dispatch, push] on: [pull_request, workflow_dispatch]
jobs: jobs:
test: test:
@ -10,15 +10,9 @@ jobs:
fail-fast: false fail-fast: false
matrix: matrix:
include: container: [1, 2, 3]
- TYPE: "server"
JOB_NAME: "Server"
RUN_COMMAND: cd ~/frappe-bench/ && bench --site test_site run-tests --app erpnext --coverage
- TYPE: "patch"
JOB_NAME: "Patch"
RUN_COMMAND: cd ~/frappe-bench/ && wget http://build.erpnext.com/20171108_190013_955977f8_database.sql.gz && bench --site test_site --force restore ~/frappe-bench/20171108_190013_955977f8_database.sql.gz && bench --site test_site migrate
name: ${{ matrix.JOB_NAME }} name: Python Unit Tests
services: services:
mysql: mysql:
@ -36,7 +30,7 @@ jobs:
- name: Setup Python - name: Setup Python
uses: actions/setup-python@v2 uses: actions/setup-python@v2
with: with:
python-version: 3.6 python-version: 3.7
- name: Add to Hosts - name: Add to Hosts
run: echo "127.0.0.1 test_site" | sudo tee -a /etc/hosts run: echo "127.0.0.1 test_site" | sudo tee -a /etc/hosts
@ -49,6 +43,7 @@ jobs:
restore-keys: | restore-keys: |
${{ runner.os }}-pip- ${{ runner.os }}-pip-
${{ runner.os }}- ${{ runner.os }}-
- name: Cache node modules - name: Cache node modules
uses: actions/cache@v2 uses: actions/cache@v2
env: env:
@ -60,6 +55,7 @@ jobs:
${{ runner.os }}-build-${{ env.cache-name }}- ${{ runner.os }}-build-${{ env.cache-name }}-
${{ runner.os }}-build- ${{ runner.os }}-build-
${{ runner.os }}- ${{ runner.os }}-
- name: Get yarn cache directory path - name: Get yarn cache directory path
id: yarn-cache-dir-path id: yarn-cache-dir-path
run: echo "::set-output name=dir::$(yarn cache dir)" run: echo "::set-output name=dir::$(yarn cache dir)"
@ -76,33 +72,39 @@ jobs:
run: bash ${GITHUB_WORKSPACE}/.github/helper/install.sh run: bash ${GITHUB_WORKSPACE}/.github/helper/install.sh
- name: Run Tests - name: Run Tests
run: ${{ matrix.RUN_COMMAND }} run: cd ~/frappe-bench/ && bench --site test_site run-parallel-tests --app erpnext --use-orchestrator --with-coverage
env: env:
TYPE: ${{ matrix.TYPE }} TYPE: server
CI_BUILD_ID: ${{ github.run_id }}
ORCHESTRATOR_URL: http://test-orchestrator.frappe.io
- name: Coverage - Pull Request - name: Upload Coverage Data
if: matrix.TYPE == 'server' && github.event_name == 'pull_request'
run: | run: |
cp ~/frappe-bench/sites/.coverage ${GITHUB_WORKSPACE} cp ~/frappe-bench/sites/.coverage ${GITHUB_WORKSPACE}
cd ${GITHUB_WORKSPACE} cd ${GITHUB_WORKSPACE}
pip install coveralls==2.2.0 pip3 install coverage==5.5
pip install coverage==4.5.4 pip3 install coveralls==3.0.1
coveralls --service=github coveralls
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
COVERALLS_REPO_TOKEN: ${{ secrets.COVERALLS_TOKEN }} COVERALLS_FLAG_NAME: run-${{ matrix.container }}
COVERALLS_SERVICE_NAME: github COVERALLS_SERVICE_NAME: ${{ github.event_name == 'pull_request' && 'github' || 'github-actions' }}
COVERALLS_PARALLEL: true
- name: Coverage - Push coveralls:
if: matrix.TYPE == 'server' && github.event_name == 'push' name: Coverage Wrap Up
needs: test
container: python:3-slim
runs-on: ubuntu-18.04
steps:
- name: Clone
uses: actions/checkout@v2
- name: Coveralls Finished
run: | run: |
cp ~/frappe-bench/sites/.coverage ${GITHUB_WORKSPACE}
cd ${GITHUB_WORKSPACE} cd ${GITHUB_WORKSPACE}
pip install coveralls==2.2.0 pip3 install coverage==5.5
pip install coverage==4.5.4 pip3 install coveralls==3.0.1
coveralls --service=github-actions coveralls --finish
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
COVERALLS_REPO_TOKEN: ${{ secrets.COVERALLS_TOKEN }}
COVERALLS_SERVICE_NAME: github-actions

View File

@ -7,7 +7,8 @@ import frappe
import unittest import unittest
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
from erpnext.accounts.doctype.journal_entry.test_journal_entry import make_journal_entry from erpnext.accounts.doctype.journal_entry.test_journal_entry import make_journal_entry
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import delete_accounting_dimension
test_dependencies = ['Cost Center', 'Location', 'Warehouse', 'Department']
class TestAccountingDimension(unittest.TestCase): class TestAccountingDimension(unittest.TestCase):
def setUp(self): def setUp(self):

View File

@ -9,6 +9,8 @@ from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sal
from erpnext.accounts.doctype.accounting_dimension.test_accounting_dimension import create_dimension, disable_dimension from erpnext.accounts.doctype.accounting_dimension.test_accounting_dimension import create_dimension, disable_dimension
from erpnext.exceptions import InvalidAccountDimensionError, MandatoryAccountDimensionError from erpnext.exceptions import InvalidAccountDimensionError, MandatoryAccountDimensionError
test_dependencies = ['Location', 'Cost Center', 'Department']
class TestAccountingDimensionFilter(unittest.TestCase): class TestAccountingDimensionFilter(unittest.TestCase):
def setUp(self): def setUp(self):
create_dimension() create_dimension()

View File

@ -10,6 +10,8 @@ from erpnext.accounts.general_ledger import ClosedAccountingPeriod
from erpnext.accounts.doctype.accounting_period.accounting_period import OverlapError from erpnext.accounts.doctype.accounting_period.accounting_period import OverlapError
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
test_dependencies = ['Item']
class TestAccountingPeriod(unittest.TestCase): class TestAccountingPeriod(unittest.TestCase):
def test_overlap(self): def test_overlap(self):
ap1 = create_accounting_period(start_date = "2018-04-01", ap1 = create_accounting_period(start_date = "2018-04-01",

View File

@ -18,6 +18,7 @@
"delete_linked_ledger_entries", "delete_linked_ledger_entries",
"book_asset_depreciation_entry_automatically", "book_asset_depreciation_entry_automatically",
"unlink_advance_payment_on_cancelation_of_order", "unlink_advance_payment_on_cancelation_of_order",
"post_change_gl_entries",
"tax_settings_section", "tax_settings_section",
"determine_address_tax_category_from", "determine_address_tax_category_from",
"column_break_19", "column_break_19",
@ -253,6 +254,12 @@
{ {
"fieldname": "column_break_19", "fieldname": "column_break_19",
"fieldtype": "Column Break" "fieldtype": "Column Break"
},
{
"default": "1",
"fieldname": "post_change_gl_entries",
"fieldtype": "Check",
"label": "Post Ledger Entries for Given Change"
} }
], ],
"icon": "icon-cog", "icon": "icon-cog",
@ -260,7 +267,7 @@
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"issingle": 1, "issingle": 1,
"links": [], "links": [],
"modified": "2021-04-30 15:25:10.381008", "modified": "2021-05-25 12:34:05.858669",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Accounts Settings", "name": "Accounts Settings",

View File

@ -11,6 +11,8 @@ from erpnext.buying.doctype.purchase_order.test_purchase_order import create_pur
from erpnext.accounts.doctype.budget.budget import get_actual_expense, BudgetError from erpnext.accounts.doctype.budget.budget import get_actual_expense, BudgetError
from erpnext.accounts.doctype.journal_entry.test_journal_entry import make_journal_entry from erpnext.accounts.doctype.journal_entry.test_journal_entry import make_journal_entry
test_dependencies = ['Monthly Distribution']
class TestBudget(unittest.TestCase): class TestBudget(unittest.TestCase):
def test_monthly_budget_crossed_ignore(self): def test_monthly_budget_crossed_ignore(self):
set_total_expense_zero(nowdate(), "cost_center") set_total_expense_zero(nowdate(), "cost_center")

View File

@ -293,7 +293,7 @@ def validate_accounts(file_name):
accounts_dict = {} accounts_dict = {}
for account in accounts: for account in accounts:
accounts_dict.setdefault(account["account_name"], account) accounts_dict.setdefault(account["account_name"], account)
if not hasattr(account, "parent_account"): if "parent_account" not in account:
msg = _("Please make sure the file you are using has 'Parent Account' column present in the header.") msg = _("Please make sure the file you are using has 'Parent Account' column present in the header.")
msg += "<br><br>" msg += "<br><br>"
msg += _("Alternatively, you can download the template and fill your data in.") msg += _("Alternatively, you can download the template and fill your data in.")

View File

@ -49,11 +49,11 @@ class InvoiceDiscounting(AccountsController):
self.make_gl_entries() self.make_gl_entries()
def on_cancel(self): def on_cancel(self):
self.set_status() self.set_status(cancel=1)
self.update_sales_invoice() self.update_sales_invoice()
self.make_gl_entries() self.make_gl_entries()
def set_status(self, status=None): def set_status(self, status=None, cancel=0):
if status: if status:
self.status = status self.status = status
self.db_set("status", status) self.db_set("status", status)
@ -66,6 +66,9 @@ class InvoiceDiscounting(AccountsController):
elif self.docstatus == 2: elif self.docstatus == 2:
self.status = "Cancelled" self.status = "Cancelled"
if cancel:
self.db_set('status', self.status, update_modified = True)
def update_sales_invoice(self): def update_sales_invoice(self):
for d in self.invoices: for d in self.invoices:
if self.docstatus == 1: if self.docstatus == 1:

View File

@ -0,0 +1,17 @@
frappe.ui.form.on("Journal Entry", {
refresh: function(frm) {
frm.set_query('company_address', function(doc) {
if(!doc.company) {
frappe.throw(__('Please set Company'));
}
return {
query: 'frappe.contacts.doctype.address.address.address_query',
filters: {
link_doctype: 'Company',
link_name: doc.company
}
};
});
}
});

View File

@ -65,7 +65,6 @@ class PaymentEntry(AccountsController):
self.set_status() self.set_status()
def on_submit(self): def on_submit(self):
self.setup_party_account_field()
if self.difference_amount: if self.difference_amount:
frappe.throw(_("Difference Amount must be zero")) frappe.throw(_("Difference Amount must be zero"))
self.make_gl_entries() self.make_gl_entries()
@ -78,7 +77,6 @@ class PaymentEntry(AccountsController):
def on_cancel(self): def on_cancel(self):
self.ignore_linked_doctypes = ('GL Entry', 'Stock Ledger Entry') self.ignore_linked_doctypes = ('GL Entry', 'Stock Ledger Entry')
self.setup_party_account_field()
self.make_gl_entries(cancel=1) self.make_gl_entries(cancel=1)
self.update_outstanding_amounts() self.update_outstanding_amounts()
self.update_advance_paid() self.update_advance_paid()
@ -122,6 +120,11 @@ class PaymentEntry(AccountsController):
if flt(d.allocated_amount) > flt(d.outstanding_amount): if flt(d.allocated_amount) > flt(d.outstanding_amount):
frappe.throw(_("Row #{0}: Allocated Amount cannot be greater than outstanding amount.").format(d.idx)) frappe.throw(_("Row #{0}: Allocated Amount cannot be greater than outstanding amount.").format(d.idx))
# Check for negative outstanding invoices as well
if flt(d.allocated_amount) < 0:
if flt(d.allocated_amount) < flt(d.outstanding_amount):
frappe.throw(_("Row #{0}: Allocated Amount cannot be greater than outstanding amount.").format(d.idx))
def delink_advance_entry_references(self): def delink_advance_entry_references(self):
for reference in self.references: for reference in self.references:
if reference.reference_doctype in ("Sales Invoice", "Purchase Invoice"): if reference.reference_doctype in ("Sales Invoice", "Purchase Invoice"):
@ -177,7 +180,7 @@ class PaymentEntry(AccountsController):
for field, value in iteritems(ref_details): for field, value in iteritems(ref_details):
if field == 'exchange_rate' or not d.get(field) or force: if field == 'exchange_rate' or not d.get(field) or force:
d.set(field, value) d.db_set(field, value)
def validate_payment_type(self): def validate_payment_type(self):
if self.payment_type not in ("Receive", "Pay", "Internal Transfer"): if self.payment_type not in ("Receive", "Pay", "Internal Transfer"):
@ -386,6 +389,8 @@ class PaymentEntry(AccountsController):
else: else:
self.status = 'Draft' self.status = 'Draft'
self.db_set('status', self.status, update_modified = True)
def set_amounts(self): def set_amounts(self):
self.set_amounts_in_company_currency() self.set_amounts_in_company_currency()
self.set_total_allocated_amount() self.set_total_allocated_amount()
@ -989,6 +994,7 @@ def get_reference_details(reference_doctype, reference_name, party_account_curre
outstanding_amount = ref_doc.get("outstanding_amount") outstanding_amount = ref_doc.get("outstanding_amount")
elif reference_doctype == "Donation": elif reference_doctype == "Donation":
total_amount = ref_doc.get("amount") total_amount = ref_doc.get("amount")
outstanding_amount = total_amount
exchange_rate = 1 exchange_rate = 1
elif reference_doctype == "Dunning": elif reference_doctype == "Dunning":
total_amount = ref_doc.get("dunning_amount") total_amount = ref_doc.get("dunning_amount")

View File

@ -492,7 +492,6 @@ def update_payment_req_status(doc, method):
status = 'Requested' status = 'Requested'
pay_req_doc.db_set('status', status) pay_req_doc.db_set('status', status)
frappe.db.commit()
def get_dummy_message(doc): def get_dummy_message(doc):
return frappe.render_template("""{% if doc.contact_person -%} return frappe.render_template("""{% if doc.contact_person -%}

View File

@ -1,297 +1,102 @@
{ {
"allow_copy": 0, "actions": [],
"allow_guest_to_view": 0,
"allow_import": 0,
"allow_rename": 0,
"autoname": "ACC-PCV-.YYYY.-.#####", "autoname": "ACC-PCV-.YYYY.-.#####",
"beta": 0,
"creation": "2013-01-10 16:34:07", "creation": "2013-01-10 16:34:07",
"custom": 0,
"docstatus": 0,
"doctype": "DocType", "doctype": "DocType",
"editable_grid": 0,
"engine": "InnoDB", "engine": "InnoDB",
"field_order": [
"transaction_date",
"posting_date",
"fiscal_year",
"amended_from",
"company",
"cost_center_wise_pnl",
"column_break1",
"closing_account_head",
"remarks"
],
"fields": [ "fields": [
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "transaction_date", "fieldname": "transaction_date",
"fieldtype": "Date", "fieldtype": "Date",
"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,
"label": "Transaction Date", "label": "Transaction Date",
"length": 0,
"no_copy": 0,
"oldfieldname": "transaction_date", "oldfieldname": "transaction_date",
"oldfieldtype": "Date", "oldfieldtype": "Date"
"permlevel": 0,
"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,
"translatable": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "posting_date", "fieldname": "posting_date",
"fieldtype": "Date", "fieldtype": "Date",
"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,
"label": "Posting Date", "label": "Posting Date",
"length": 0,
"no_copy": 0,
"oldfieldname": "posting_date", "oldfieldname": "posting_date",
"oldfieldtype": "Date", "oldfieldtype": "Date",
"permlevel": 0, "reqd": 1
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "fiscal_year", "fieldname": "fiscal_year",
"fieldtype": "Link", "fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1, "in_list_view": 1,
"in_standard_filter": 1, "in_standard_filter": 1,
"label": "Closing Fiscal Year", "label": "Closing Fiscal Year",
"length": 0,
"no_copy": 0,
"oldfieldname": "fiscal_year", "oldfieldname": "fiscal_year",
"oldfieldtype": "Select", "oldfieldtype": "Select",
"options": "Fiscal Year", "options": "Fiscal Year",
"permlevel": 0, "reqd": 1
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "amended_from", "fieldname": "amended_from",
"fieldtype": "Link", "fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 1, "ignore_user_permissions": 1,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Amended From", "label": "Amended From",
"length": 0,
"no_copy": 1, "no_copy": 1,
"oldfieldname": "amended_from", "oldfieldname": "amended_from",
"oldfieldtype": "Data", "oldfieldtype": "Data",
"options": "Period Closing Voucher", "options": "Period Closing Voucher",
"permlevel": 0, "read_only": 1
"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,
"translatable": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "company", "fieldname": "company",
"fieldtype": "Link", "fieldtype": "Link",
"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,
"label": "Company", "label": "Company",
"length": 0,
"no_copy": 0,
"oldfieldname": "company", "oldfieldname": "company",
"oldfieldtype": "Select", "oldfieldtype": "Select",
"options": "Company", "options": "Company",
"permlevel": 0, "reqd": 1
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "column_break1", "fieldname": "column_break1",
"fieldtype": "Column Break", "fieldtype": "Column Break",
"hidden": 0, "oldfieldtype": "Column Break"
"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,
"oldfieldtype": "Column Break",
"permlevel": 0,
"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,
"translatable": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"description": "The account head under Liability or Equity, in which Profit/Loss will be booked", "description": "The account head under Liability or Equity, in which Profit/Loss will be booked",
"fieldname": "closing_account_head", "fieldname": "closing_account_head",
"fieldtype": "Link", "fieldtype": "Link",
"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,
"label": "Closing Account Head", "label": "Closing Account Head",
"length": 0,
"no_copy": 0,
"oldfieldname": "closing_account_head", "oldfieldname": "closing_account_head",
"oldfieldtype": "Link", "oldfieldtype": "Link",
"options": "Account", "options": "Account",
"permlevel": 0, "reqd": 1
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "remarks", "fieldname": "remarks",
"fieldtype": "Small Text", "fieldtype": "Small Text",
"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,
"label": "Remarks", "label": "Remarks",
"length": 0,
"no_copy": 0,
"oldfieldname": "remarks", "oldfieldname": "remarks",
"oldfieldtype": "Small Text", "oldfieldtype": "Small Text",
"permlevel": 0, "reqd": 1
"print_hide": 0, },
"print_hide_if_no_value": 0, {
"read_only": 0, "default": "0",
"remember_last_selected_value": 0, "fieldname": "cost_center_wise_pnl",
"report_hide": 0, "fieldtype": "Check",
"reqd": 1, "label": "Book Cost Center Wise Profit/Loss"
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
} }
], ],
"has_web_view": 0,
"hide_heading": 0,
"hide_toolbar": 0,
"icon": "fa fa-file-text", "icon": "fa fa-file-text",
"idx": 1, "idx": 1,
"image_view": 0,
"in_create": 0,
"is_submittable": 1, "is_submittable": 1,
"issingle": 0, "links": [],
"istable": 0, "modified": "2021-05-20 15:27:37.210458",
"max_attachments": 0,
"modified": "2020-09-18 17:26:09.703215",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Period Closing Voucher", "name": "Period Closing Voucher",
@ -303,15 +108,10 @@
"create": 1, "create": 1,
"delete": 1, "delete": 1,
"email": 1, "email": 1,
"export": 0,
"if_owner": 0,
"import": 0,
"permlevel": 0,
"print": 1, "print": 1,
"read": 1, "read": 1,
"report": 1, "report": 1,
"role": "System Manager", "role": "System Manager",
"set_user_permissions": 0,
"share": 1, "share": 1,
"submit": 1, "submit": 1,
"write": 1 "write": 1
@ -322,29 +122,17 @@
"create": 1, "create": 1,
"delete": 1, "delete": 1,
"email": 1, "email": 1,
"export": 0,
"if_owner": 0,
"import": 0,
"permlevel": 0,
"print": 1, "print": 1,
"read": 1, "read": 1,
"report": 1, "report": 1,
"role": "Accounts Manager", "role": "Accounts Manager",
"set_user_permissions": 0,
"share": 1, "share": 1,
"submit": 1, "submit": 1,
"write": 1 "write": 1
} }
], ],
"quick_entry": 0,
"read_only": 0,
"read_only_onload": 0,
"search_fields": "posting_date, fiscal_year", "search_fields": "posting_date, fiscal_year",
"show_name_in_global_search": 0,
"sort_field": "modified", "sort_field": "modified",
"sort_order": "DESC", "sort_order": "DESC",
"title_field": "closing_account_head", "title_field": "closing_account_head"
"track_changes": 0,
"track_seen": 0,
"track_views": 0
} }

View File

@ -52,35 +52,35 @@ class PeriodClosingVoucher(AccountsController):
def make_gl_entries(self): def make_gl_entries(self):
gl_entries = [] gl_entries = []
net_pl_balance = 0 net_pl_balance = 0
dimension_fields = ['t1.cost_center']
accounting_dimensions = get_accounting_dimensions() pl_accounts = self.get_pl_balances()
for dimension in accounting_dimensions:
dimension_fields.append('t1.{0}'.format(dimension))
dimension_filters, default_dimensions = get_dimensions()
pl_accounts = self.get_pl_balances(dimension_fields)
for acc in pl_accounts: for acc in pl_accounts:
if flt(acc.balance_in_company_currency): if flt(acc.bal_in_company_currency):
gl_entries.append(self.get_gl_dict({ gl_entries.append(self.get_gl_dict({
"account": acc.account, "account": acc.account,
"cost_center": acc.cost_center, "cost_center": acc.cost_center,
"account_currency": acc.account_currency, "account_currency": acc.account_currency,
"debit_in_account_currency": abs(flt(acc.balance_in_account_currency)) \ "debit_in_account_currency": abs(flt(acc.bal_in_account_currency)) if flt(acc.bal_in_account_currency) < 0 else 0,
if flt(acc.balance_in_account_currency) < 0 else 0, "debit": abs(flt(acc.bal_in_company_currency)) if flt(acc.bal_in_company_currency) < 0 else 0,
"debit": abs(flt(acc.balance_in_company_currency)) \ "credit_in_account_currency": abs(flt(acc.bal_in_account_currency)) if flt(acc.bal_in_account_currency) > 0 else 0,
if flt(acc.balance_in_company_currency) < 0 else 0, "credit": abs(flt(acc.bal_in_company_currency)) if flt(acc.bal_in_company_currency) > 0 else 0
"credit_in_account_currency": abs(flt(acc.balance_in_account_currency)) \
if flt(acc.balance_in_account_currency) > 0 else 0,
"credit": abs(flt(acc.balance_in_company_currency)) \
if flt(acc.balance_in_company_currency) > 0 else 0
}, item=acc)) }, item=acc))
net_pl_balance += flt(acc.balance_in_company_currency) net_pl_balance += flt(acc.bal_in_company_currency)
if net_pl_balance: if net_pl_balance:
if self.cost_center_wise_pnl:
costcenter_wise_gl_entries = self.get_costcenter_wise_pnl_gl_entries(pl_accounts)
gl_entries += costcenter_wise_gl_entries
else:
gl_entry = self.get_pnl_gl_entry(net_pl_balance)
gl_entries.append(gl_entry)
from erpnext.accounts.general_ledger import make_gl_entries
make_gl_entries(gl_entries)
def get_pnl_gl_entry(self, net_pl_balance):
cost_center = frappe.db.get_value("Company", self.company, "cost_center") cost_center = frappe.db.get_value("Company", self.company, "cost_center")
gl_entry = self.get_gl_dict({ gl_entry = self.get_gl_dict({
"account": self.closing_account_head, "account": self.closing_account_head,
@ -91,23 +91,56 @@ class PeriodClosingVoucher(AccountsController):
"cost_center": cost_center "cost_center": cost_center
}) })
for dimension in accounting_dimensions: self.update_default_dimensions(gl_entry)
return gl_entry
def get_costcenter_wise_pnl_gl_entries(self, pl_accounts):
company_cost_center = frappe.db.get_value("Company", self.company, "cost_center")
gl_entries = []
for acc in pl_accounts:
if flt(acc.bal_in_company_currency):
gl_entry = self.get_gl_dict({
"account": self.closing_account_head,
"cost_center": acc.cost_center or company_cost_center,
"account_currency": acc.account_currency,
"debit_in_account_currency": abs(flt(acc.bal_in_account_currency)) if flt(acc.bal_in_account_currency) > 0 else 0,
"debit": abs(flt(acc.bal_in_company_currency)) if flt(acc.bal_in_company_currency) > 0 else 0,
"credit_in_account_currency": abs(flt(acc.bal_in_account_currency)) if flt(acc.bal_in_account_currency) < 0 else 0,
"credit": abs(flt(acc.bal_in_company_currency)) if flt(acc.bal_in_company_currency) < 0 else 0
}, item=acc)
self.update_default_dimensions(gl_entry)
gl_entries.append(gl_entry)
return gl_entries
def update_default_dimensions(self, gl_entry):
if not self.accounting_dimensions:
self.accounting_dimensions = get_accounting_dimensions()
_, default_dimensions = get_dimensions()
for dimension in self.accounting_dimensions:
gl_entry.update({ gl_entry.update({
dimension: default_dimensions.get(self.company, {}).get(dimension) dimension: default_dimensions.get(self.company, {}).get(dimension)
}) })
gl_entries.append(gl_entry) def get_pl_balances(self):
"""Get balance for dimension-wise pl accounts"""
from erpnext.accounts.general_ledger import make_gl_entries dimension_fields = ['t1.cost_center']
make_gl_entries(gl_entries)
self.accounting_dimensions = get_accounting_dimensions()
for dimension in self.accounting_dimensions:
dimension_fields.append('t1.{0}'.format(dimension))
def get_pl_balances(self, dimension_fields):
"""Get balance for pl accounts"""
return frappe.db.sql(""" return frappe.db.sql("""
select select
t1.account, t2.account_currency, {dimension_fields}, t1.account, t2.account_currency, {dimension_fields},
sum(t1.debit_in_account_currency) - sum(t1.credit_in_account_currency) as balance_in_account_currency, sum(t1.debit_in_account_currency) - sum(t1.credit_in_account_currency) as bal_in_account_currency,
sum(t1.debit) - sum(t1.credit) as balance_in_company_currency sum(t1.debit) - sum(t1.credit) as bal_in_company_currency
from `tabGL Entry` t1, `tabAccount` t2 from `tabGL Entry` t1, `tabAccount` t2
where t1.account = t2.name and t2.report_type = 'Profit and Loss' where t1.account = t2.name and t2.report_type = 'Profit and Loss'
and t2.docstatus < 2 and t2.company = %s and t2.docstatus < 2 and t2.company = %s

View File

@ -8,6 +8,7 @@ import frappe
from frappe.utils import flt, today from frappe.utils import flt, today
from erpnext.accounts.utils import get_fiscal_year, now from erpnext.accounts.utils import get_fiscal_year, now
from erpnext.accounts.doctype.journal_entry.test_journal_entry import make_journal_entry from erpnext.accounts.doctype.journal_entry.test_journal_entry import make_journal_entry
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
class TestPeriodClosingVoucher(unittest.TestCase): class TestPeriodClosingVoucher(unittest.TestCase):
def test_closing_entry(self): def test_closing_entry(self):
@ -65,6 +66,58 @@ class TestPeriodClosingVoucher(unittest.TestCase):
self.assertEqual(gle_for_random_expense_account[0].amount_in_account_currency, self.assertEqual(gle_for_random_expense_account[0].amount_in_account_currency,
-1*random_expense_account[0].balance_in_account_currency) -1*random_expense_account[0].balance_in_account_currency)
def test_cost_center_wise_posting(self):
frappe.db.sql("delete from `tabGL Entry` where company='Test PCV Company'")
company = create_company()
surplus_account = create_account()
cost_center1 = create_cost_center("Test Cost Center 1")
cost_center2 = create_cost_center("Test Cost Center 2")
create_sales_invoice(
company=company,
cost_center=cost_center1,
income_account="Sales - TPC",
expense_account="Cost of Goods Sold - TPC",
rate=400,
debit_to="Debtors - TPC"
)
create_sales_invoice(
company=company,
cost_center=cost_center2,
income_account="Sales - TPC",
expense_account="Cost of Goods Sold - TPC",
rate=200,
debit_to="Debtors - TPC"
)
pcv = frappe.get_doc({
"transaction_date": today(),
"posting_date": today(),
"fiscal_year": get_fiscal_year(today())[0],
"company": "Test PCV Company",
"cost_center_wise_pnl": 1,
"closing_account_head": surplus_account,
"remarks": "Test",
"doctype": "Period Closing Voucher"
})
pcv.insert()
pcv.submit()
expected_gle = (
('Sales - TPC', 200.0, 0.0, cost_center2),
(surplus_account, 0.0, 200.0, cost_center2),
('Sales - TPC', 400.0, 0.0, cost_center1),
(surplus_account, 0.0, 400.0, cost_center1)
)
pcv_gle = frappe.db.sql("""
select account, debit, credit, cost_center from `tabGL Entry` where voucher_no=%s
""", (pcv.name))
self.assertTrue(pcv_gle, expected_gle)
def make_period_closing_voucher(self): def make_period_closing_voucher(self):
pcv = frappe.get_doc({ pcv = frappe.get_doc({
"doctype": "Period Closing Voucher", "doctype": "Period Closing Voucher",
@ -80,6 +133,38 @@ class TestPeriodClosingVoucher(unittest.TestCase):
return pcv return pcv
def create_company():
company = frappe.get_doc({
'doctype': 'Company',
'company_name': "Test PCV Company",
'country': 'United States',
'default_currency': 'USD'
})
company.insert(ignore_if_duplicate = True)
return company.name
def create_account():
account = frappe.get_doc({
"account_name": "Reserve and Surplus",
"is_group": 0,
"company": "Test PCV Company",
"root_type": "Liability",
"report_type": "Balance Sheet",
"account_currency": "USD",
"parent_account": "Current Liabilities - TPC",
"doctype": "Account"
}).insert(ignore_if_duplicate = True)
return account.name
def create_cost_center(cc_name):
costcenter = frappe.get_doc({
"company": "Test PCV Company",
"cost_center_name": cc_name,
"doctype": "Cost Center",
"parent_cost_center": "Test PCV Company - TPC"
})
costcenter.insert(ignore_if_duplicate = True)
return costcenter.name
test_dependencies = ["Customer", "Cost Center"] test_dependencies = ["Customer", "Cost Center"]
test_records = frappe.get_test_records("Period Closing Voucher") test_records = frappe.get_test_records("Period Closing Voucher")

View File

@ -107,7 +107,7 @@ frappe.ui.form.on('POS Closing Entry', {
frm.set_value("taxes", []); frm.set_value("taxes", []);
for (let row of frm.doc.payment_reconciliation) { for (let row of frm.doc.payment_reconciliation) {
row.expected_amount = 0; row.expected_amount = row.opening_amount;
} }
for (let row of frm.doc.pos_transactions) { for (let row of frm.doc.pos_transactions) {
@ -154,6 +154,9 @@ function add_to_pos_transaction(d, frm) {
function refresh_payments(d, frm) { function refresh_payments(d, frm) {
d.payments.forEach(p => { d.payments.forEach(p => {
const payment = frm.doc.payment_reconciliation.find(pay => pay.mode_of_payment === p.mode_of_payment); const payment = frm.doc.payment_reconciliation.find(pay => pay.mode_of_payment === p.mode_of_payment);
if (p.account == d.account_for_change_amount) {
p.amount -= flt(d.change_amount);
}
if (payment) { if (payment) {
payment.expected_amount += flt(p.amount); payment.expected_amount += flt(p.amount);
payment.difference = payment.closing_amount - payment.expected_amount; payment.difference = payment.closing_amount - payment.expected_amount;

View File

@ -140,6 +140,7 @@ class POSInvoice(SalesInvoice):
return return
available_stock = get_stock_availability(d.item_code, d.warehouse) available_stock = get_stock_availability(d.item_code, d.warehouse)
item_code, warehouse, qty = frappe.bold(d.item_code), frappe.bold(d.warehouse), frappe.bold(d.qty) item_code, warehouse, qty = frappe.bold(d.item_code), frappe.bold(d.warehouse), frappe.bold(d.qty)
if flt(available_stock) <= 0: if flt(available_stock) <= 0:
frappe.throw(_('Row #{}: Item Code: {} is not available under warehouse {}.') frappe.throw(_('Row #{}: Item Code: {} is not available under warehouse {}.')
@ -213,6 +214,7 @@ class POSInvoice(SalesInvoice):
for d in self.get("items"): for d in self.get("items"):
is_stock_item = frappe.get_cached_value("Item", d.get("item_code"), "is_stock_item") is_stock_item = frappe.get_cached_value("Item", d.get("item_code"), "is_stock_item")
if not is_stock_item: if not is_stock_item:
if not frappe.db.exists('Product Bundle', d.item_code):
frappe.throw(_("Row #{}: Item {} is a non stock item. You can only include stock items in a POS Invoice.") frappe.throw(_("Row #{}: Item {} is a non stock item. You can only include stock items in a POS Invoice.")
.format(d.idx, frappe.bold(d.item_code)), title=_("Invalid Item")) .format(d.idx, frappe.bold(d.item_code)), title=_("Invalid Item"))
@ -455,15 +457,36 @@ class POSInvoice(SalesInvoice):
@frappe.whitelist() @frappe.whitelist()
def get_stock_availability(item_code, warehouse): def get_stock_availability(item_code, warehouse):
if frappe.db.get_value('Item', item_code, 'is_stock_item'):
bin_qty = get_bin_qty(item_code, warehouse)
pos_sales_qty = get_pos_reserved_qty(item_code, warehouse)
return bin_qty - pos_sales_qty
else:
if frappe.db.exists('Product Bundle', item_code):
return get_bundle_availability(item_code, warehouse)
def get_bundle_availability(bundle_item_code, warehouse):
product_bundle = frappe.get_doc('Product Bundle', bundle_item_code)
bundle_bin_qty = 1000000
for item in product_bundle.items:
item_bin_qty = get_bin_qty(item.item_code, warehouse)
item_pos_reserved_qty = get_pos_reserved_qty(item.item_code, warehouse)
available_qty = item_bin_qty - item_pos_reserved_qty
max_available_bundles = available_qty / item.qty
if bundle_bin_qty > max_available_bundles:
bundle_bin_qty = max_available_bundles
pos_sales_qty = get_pos_reserved_qty(bundle_item_code, warehouse)
return bundle_bin_qty - pos_sales_qty
def get_bin_qty(item_code, warehouse):
bin_qty = frappe.db.sql("""select actual_qty from `tabBin` bin_qty = frappe.db.sql("""select actual_qty from `tabBin`
where item_code = %s and warehouse = %s where item_code = %s and warehouse = %s
limit 1""", (item_code, warehouse), as_dict=1) limit 1""", (item_code, warehouse), as_dict=1)
pos_sales_qty = get_pos_reserved_qty(item_code, warehouse) return bin_qty[0].actual_qty or 0 if bin_qty else 0
bin_qty = bin_qty[0].actual_qty or 0 if bin_qty else 0
return bin_qty - pos_sales_qty
def get_pos_reserved_qty(item_code, warehouse): def get_pos_reserved_qty(item_code, warehouse):
reserved_qty = frappe.db.sql("""select sum(p_item.qty) as qty reserved_qty = frappe.db.sql("""select sum(p_item.qty) as qty

View File

@ -152,7 +152,7 @@ class PricingRule(Document):
frappe.throw(_("Valid from date must be less than valid upto date")) frappe.throw(_("Valid from date must be less than valid upto date"))
def validate_condition(self): def validate_condition(self):
if self.condition and ("=" in self.condition) and re.match("""[\w\.:_]+\s*={1}\s*[\w\.@'"]+""", self.condition): if self.condition and ("=" in self.condition) and re.match(r'[\w\.:_]+\s*={1}\s*[\w\.@\'"]+', self.condition):
frappe.throw(_("Invalid condition expression")) frappe.throw(_("Invalid condition expression"))
#-------------------------------------------------------------------------------- #--------------------------------------------------------------------------------

View File

@ -837,6 +837,7 @@
"read_only": 1 "read_only": 1
}, },
{ {
"depends_on": "eval:!doc.disable_rounded_total",
"fieldname": "base_rounding_adjustment", "fieldname": "base_rounding_adjustment",
"fieldtype": "Currency", "fieldtype": "Currency",
"label": "Rounding Adjustment (Company Currency)", "label": "Rounding Adjustment (Company Currency)",
@ -883,6 +884,7 @@
"read_only": 1 "read_only": 1
}, },
{ {
"depends_on": "eval:!doc.disable_rounded_total",
"fieldname": "rounding_adjustment", "fieldname": "rounding_adjustment",
"fieldtype": "Currency", "fieldtype": "Currency",
"label": "Rounding Adjustment", "label": "Rounding Adjustment",
@ -1380,7 +1382,7 @@
"idx": 204, "idx": 204,
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2021-04-30 22:45:58.334107", "modified": "2021-06-09 12:30:25.632109",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Purchase Invoice", "name": "Purchase Invoice",

View File

@ -636,8 +636,8 @@ class TestPurchaseInvoice(unittest.TestCase):
def test_rejected_serial_no(self): def test_rejected_serial_no(self):
pi = make_purchase_invoice(item_code="_Test Serialized Item With Series", received_qty=2, qty=1, pi = make_purchase_invoice(item_code="_Test Serialized Item With Series", received_qty=2, qty=1,
rejected_qty=1, rate=500, update_stock=1, rejected_qty=1, rate=500, update_stock=1, rejected_warehouse = "_Test Rejected Warehouse - _TC",
rejected_warehouse = "_Test Rejected Warehouse - _TC") allow_zero_valuation_rate=1)
self.assertEqual(frappe.db.get_value("Serial No", pi.get("items")[0].serial_no, "warehouse"), self.assertEqual(frappe.db.get_value("Serial No", pi.get("items")[0].serial_no, "warehouse"),
pi.get("items")[0].warehouse) pi.get("items")[0].warehouse)
@ -994,7 +994,8 @@ def make_purchase_invoice(**args):
"project": args.project, "project": args.project,
"rejected_warehouse": args.rejected_warehouse or "", "rejected_warehouse": args.rejected_warehouse or "",
"rejected_serial_no": args.rejected_serial_no or "", "rejected_serial_no": args.rejected_serial_no or "",
"asset_location": args.location or "" "asset_location": args.location or "",
"allow_zero_valuation_rate": args.get("allow_zero_valuation_rate") or 0
}) })
if args.get_taxes_and_charges: if args.get_taxes_and_charges:

View File

@ -17,7 +17,7 @@ erpnext.accounts.SalesInvoiceController = erpnext.selling.SellingController.exte
var me = this; var me = this;
this._super(); this._super();
this.frm.ignore_doctypes_on_cancel_all = ['POS Invoice', 'Timesheet']; this.frm.ignore_doctypes_on_cancel_all = ['POS Invoice', 'Timesheet', 'POS Invoice Merge Log', 'POS Closing Entry'];
if(!this.frm.doc.__islocal && !this.frm.doc.customer && this.frm.doc.debit_to) { if(!this.frm.doc.__islocal && !this.frm.doc.customer && this.frm.doc.debit_to) {
// show debit_to in print format // show debit_to in print format
this.frm.set_df_property("debit_to", "print_hide", 0); this.frm.set_df_property("debit_to", "print_hide", 0);

View File

@ -531,7 +531,7 @@ class SalesInvoice(SellingController):
# set pos values in items # set pos values in items
for item in self.get("items"): for item in self.get("items"):
if item.get('item_code'): if item.get('item_code'):
profile_details = get_pos_profile_item_details(pos, frappe._dict(item.as_dict()), pos) profile_details = get_pos_profile_item_details(pos, frappe._dict(item.as_dict()), pos, update_data=True)
for fname, val in iteritems(profile_details): for fname, val in iteritems(profile_details):
if (not for_validate) or (for_validate and not item.get(fname)): if (not for_validate) or (for_validate and not item.get(fname)):
item.set(fname, val) item.set(fname, val)
@ -849,7 +849,6 @@ class SalesInvoice(SellingController):
self.make_loyalty_point_redemption_gle(gl_entries) self.make_loyalty_point_redemption_gle(gl_entries)
self.make_pos_gl_entries(gl_entries) self.make_pos_gl_entries(gl_entries)
self.make_gle_for_change_amount(gl_entries)
self.make_write_off_gl_entry(gl_entries) self.make_write_off_gl_entry(gl_entries)
self.make_gle_for_rounding_adjustment(gl_entries) self.make_gle_for_rounding_adjustment(gl_entries)
@ -983,7 +982,13 @@ class SalesInvoice(SellingController):
def make_pos_gl_entries(self, gl_entries): def make_pos_gl_entries(self, gl_entries):
if cint(self.is_pos): if cint(self.is_pos):
skip_change_gl_entries = not cint(frappe.db.get_single_value('Accounts Settings', 'post_change_gl_entries'))
for payment_mode in self.payments: for payment_mode in self.payments:
if skip_change_gl_entries and payment_mode.account == self.account_for_change_amount:
payment_mode.base_amount -= self.change_amount
if payment_mode.amount: if payment_mode.amount:
# POS, make payment entries # POS, make payment entries
gl_entries.append( gl_entries.append(
@ -1015,8 +1020,11 @@ class SalesInvoice(SellingController):
}, payment_mode_account_currency, item=self) }, payment_mode_account_currency, item=self)
) )
if not skip_change_gl_entries:
self.make_gle_for_change_amount(gl_entries)
def make_gle_for_change_amount(self, gl_entries): def make_gle_for_change_amount(self, gl_entries):
if cint(self.is_pos) and self.change_amount: if self.change_amount:
if self.account_for_change_amount: if self.account_for_change_amount:
gl_entries.append( gl_entries.append(
self.get_gl_dict({ self.get_gl_dict({

View File

@ -713,7 +713,7 @@ class TestSalesInvoice(unittest.TestCase):
si.submit() si.submit()
self.assertEqual(si.paid_amount, 100.0) self.assertEqual(si.paid_amount, 100.0)
self.pos_gl_entry(si, pos, 50) self.validate_pos_gl_entry(si, pos, 50)
def test_pos_returns_with_repayment(self): def test_pos_returns_with_repayment(self):
from erpnext.accounts.doctype.sales_invoice.sales_invoice import make_sales_return from erpnext.accounts.doctype.sales_invoice.sales_invoice import make_sales_return
@ -749,7 +749,7 @@ class TestSalesInvoice(unittest.TestCase):
make_pos_profile(company="_Test Company with perpetual inventory", income_account = "Sales - TCP1", make_pos_profile(company="_Test Company with perpetual inventory", income_account = "Sales - TCP1",
expense_account = "Cost of Goods Sold - TCP1", warehouse="Stores - TCP1", cost_center = "Main - TCP1", write_off_account="_Test Write Off - TCP1") expense_account = "Cost of Goods Sold - TCP1", warehouse="Stores - TCP1", cost_center = "Main - TCP1", write_off_account="_Test Write Off - TCP1")
pr = make_purchase_receipt(company= "_Test Company with perpetual inventory", make_purchase_receipt(company= "_Test Company with perpetual inventory",
item_code= "_Test FG Item",warehouse= "Stores - TCP1", cost_center= "Main - TCP1") item_code= "_Test FG Item",warehouse= "Stores - TCP1", cost_center= "Main - TCP1")
pos = create_sales_invoice(company= "_Test Company with perpetual inventory", pos = create_sales_invoice(company= "_Test Company with perpetual inventory",
@ -770,7 +770,45 @@ class TestSalesInvoice(unittest.TestCase):
self.assertEqual(pos.grand_total, 100.0) self.assertEqual(pos.grand_total, 100.0)
self.assertEqual(pos.write_off_amount, -5) self.assertEqual(pos.write_off_amount, -5)
def pos_gl_entry(self, si, pos, cash_amount): def test_pos_with_no_gl_entry_for_change_amount(self):
frappe.db.set_value('Accounts Settings', None, 'post_change_gl_entries', 0)
make_pos_profile(company="_Test Company with perpetual inventory", income_account = "Sales - TCP1",
expense_account = "Cost of Goods Sold - TCP1", warehouse="Stores - TCP1", cost_center = "Main - TCP1", write_off_account="_Test Write Off - TCP1")
make_purchase_receipt(company= "_Test Company with perpetual inventory",
item_code= "_Test FG Item",warehouse= "Stores - TCP1", cost_center= "Main - TCP1")
pos = create_sales_invoice(company= "_Test Company with perpetual inventory",
debit_to="Debtors - TCP1", item_code= "_Test FG Item", warehouse="Stores - TCP1",
income_account = "Sales - TCP1", expense_account = "Cost of Goods Sold - TCP1",
cost_center = "Main - TCP1", do_not_save=True)
pos.is_pos = 1
pos.update_stock = 1
taxes = get_taxes_and_charges()
pos.taxes = []
for tax in taxes:
pos.append("taxes", tax)
pos.append("payments", {'mode_of_payment': 'Bank Draft', 'account': '_Test Bank - TCP1', 'amount': 50})
pos.append("payments", {'mode_of_payment': 'Cash', 'account': 'Cash - TCP1', 'amount': 60})
pos.insert()
pos.submit()
self.assertEqual(pos.grand_total, 100.0)
self.assertEqual(pos.change_amount, 10)
self.validate_pos_gl_entry(pos, pos, 60, validate_without_change_gle=True)
frappe.db.set_value('Accounts Settings', None, 'post_change_gl_entries', 1)
def validate_pos_gl_entry(self, si, pos, cash_amount, validate_without_change_gle=False):
if validate_without_change_gle:
cash_amount -= pos.change_amount
# check stock ledger entries # check stock ledger entries
sle = frappe.db.sql("""select * from `tabStock Ledger Entry` sle = frappe.db.sql("""select * from `tabStock Ledger Entry`
where voucher_type = 'Sales Invoice' and voucher_no = %s""", where voucher_type = 'Sales Invoice' and voucher_no = %s""",

View File

@ -185,10 +185,10 @@ def make_round_off_gle(gl_map, debit_credit_diff, precision):
for d in gl_map: for d in gl_map:
if d.account == round_off_account: if d.account == round_off_account:
round_off_gle = d round_off_gle = d
if d.debit_in_account_currency: if d.debit:
debit_credit_diff -= flt(d.debit_in_account_currency) debit_credit_diff -= flt(d.debit)
else: else:
debit_credit_diff += flt(d.credit_in_account_currency) debit_credit_diff += flt(d.credit)
round_off_account_exists = True round_off_account_exists = True
if round_off_account_exists and abs(debit_credit_diff) <= (1.0 / (10**precision)): if round_off_account_exists and abs(debit_credit_diff) <= (1.0 / (10**precision)):

View File

@ -584,6 +584,7 @@ class ReceivablePayableReport(object):
`tabGL Entry` `tabGL Entry`
where where
docstatus < 2 docstatus < 2
and is_cancelled = 0
and party_type=%s and party_type=%s
and (party is not null and party != '') and (party is not null and party != '')
{1} {2} {3}""" {1} {2} {3}"""

View File

@ -765,6 +765,7 @@
"read_only": 1 "read_only": 1
}, },
{ {
"depends_on": "eval:!doc.disable_rounded_total",
"fieldname": "base_rounding_adjustment", "fieldname": "base_rounding_adjustment",
"fieldtype": "Currency", "fieldtype": "Currency",
"label": "Rounding Adjustment (Company Currency)", "label": "Rounding Adjustment (Company Currency)",
@ -810,6 +811,7 @@
"read_only": 1 "read_only": 1
}, },
{ {
"depends_on": "eval:!doc.disable_rounded_total",
"fieldname": "rounding_adjustment", "fieldname": "rounding_adjustment",
"fieldtype": "Currency", "fieldtype": "Currency",
"label": "Rounding Adjustment", "label": "Rounding Adjustment",
@ -1124,7 +1126,7 @@
"idx": 105, "idx": 105,
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2021-01-20 22:07:23.487138", "modified": "2021-04-19 00:55:30.781375",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Buying", "module": "Buying",
"name": "Purchase Order", "name": "Purchase Order",

View File

@ -576,6 +576,7 @@
"read_only": 1 "read_only": 1
}, },
{ {
"depends_on": "eval:!doc.disable_rounded_total",
"fieldname": "base_rounding_adjustment", "fieldname": "base_rounding_adjustment",
"fieldtype": "Currency", "fieldtype": "Currency",
"label": "Rounding Adjustment (Company Currency", "label": "Rounding Adjustment (Company Currency",
@ -620,6 +621,7 @@
"read_only": 1 "read_only": 1
}, },
{ {
"depends_on": "eval:!doc.disable_rounded_total",
"fieldname": "rounding_adjustment", "fieldname": "rounding_adjustment",
"fieldtype": "Currency", "fieldtype": "Currency",
"label": "Rounding Adjustment", "label": "Rounding Adjustment",
@ -802,7 +804,7 @@
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2020-12-03 15:18:29.073368", "modified": "2021-04-19 00:58:20.995491",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Buying", "module": "Buying",
"name": "Supplier Quotation", "name": "Supplier Quotation",

View File

@ -9,12 +9,12 @@ from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry
from erpnext.buying.report.subcontracted_raw_materials_to_be_transferred.subcontracted_raw_materials_to_be_transferred import execute from erpnext.buying.report.subcontracted_raw_materials_to_be_transferred.subcontracted_raw_materials_to_be_transferred import execute
import json, frappe, unittest import json, frappe, unittest
class TestSubcontractedItemToBeReceived(unittest.TestCase): class TestSubcontractedItemToBeTransferred(unittest.TestCase):
def test_pending_and_received_qty(self): def test_pending_and_transferred_qty(self):
po = create_purchase_order(item_code='_Test FG Item', is_subcontracted='Yes') po = create_purchase_order(item_code='_Test FG Item', is_subcontracted='Yes')
make_stock_entry(item_code='_Test Item', target='_Test Warehouse 1 - _TC', qty=100, basic_rate=100) make_stock_entry(item_code='_Test Item', target='_Test Warehouse - _TC', qty=100, basic_rate=100)
make_stock_entry(item_code='_Test Item Home Desktop 100', target='_Test Warehouse 1 - _TC', qty=100, basic_rate=100) make_stock_entry(item_code='_Test Item Home Desktop 100', target='_Test Warehouse - _TC', qty=100, basic_rate=100)
transfer_subcontracted_raw_materials(po.name) transfer_subcontracted_raw_materials(po.name)
col, data = execute(filters=frappe._dict({'supplier': po.supplier, col, data = execute(filters=frappe._dict({'supplier': po.supplier,
'from_date': frappe.utils.get_datetime(frappe.utils.add_to_date(po.transaction_date, days=-10)), 'from_date': frappe.utils.get_datetime(frappe.utils.add_to_date(po.transaction_date, days=-10)),
@ -38,6 +38,7 @@ def transfer_subcontracted_raw_materials(po):
'warehouse': '_Test Warehouse - _TC', 'rate': 100, 'amount': 200, 'stock_uom': 'Nos'}] 'warehouse': '_Test Warehouse - _TC', 'rate': 100, 'amount': 200, 'stock_uom': 'Nos'}]
rm_item_string = json.dumps(rm_item) rm_item_string = json.dumps(rm_item)
se = frappe.get_doc(make_rm_stock_entry(po, rm_item_string)) se = frappe.get_doc(make_rm_stock_entry(po, rm_item_string))
se.from_warehouse = '_Test Warehouse 1 - _TC'
se.to_warehouse = '_Test Warehouse 1 - _TC' se.to_warehouse = '_Test Warehouse 1 - _TC'
se.stock_entry_type = 'Send to Subcontractor' se.stock_entry_type = 'Send to Subcontractor'
se.save() se.save()

View File

@ -225,7 +225,7 @@ class AccountsController(TransactionBase):
def validate_date_with_fiscal_year(self): def validate_date_with_fiscal_year(self):
if self.meta.get_field("fiscal_year"): if self.meta.get_field("fiscal_year"):
date_field = "" date_field = None
if self.meta.get_field("posting_date"): if self.meta.get_field("posting_date"):
date_field = "posting_date" date_field = "posting_date"
elif self.meta.get_field("transaction_date"): elif self.meta.get_field("transaction_date"):
@ -1011,7 +1011,6 @@ class AccountsController(TransactionBase):
else: else:
grand_total -= self.get("total_advance") grand_total -= self.get("total_advance")
base_grand_total = flt(grand_total * self.get("conversion_rate"), self.precision("base_grand_total")) base_grand_total = flt(grand_total * self.get("conversion_rate"), self.precision("base_grand_total"))
if total != flt(grand_total, self.precision("grand_total")) or \ if total != flt(grand_total, self.precision("grand_total")) or \
base_total != flt(base_grand_total, self.precision("base_grand_total")): base_total != flt(base_grand_total, self.precision("base_grand_total")):
frappe.throw(_("Total Payment Amount in Payment Schedule must be equal to Grand / Rounded Total")) frappe.throw(_("Total Payment Amount in Payment Schedule must be equal to Grand / Rounded Total"))
@ -1450,6 +1449,7 @@ def validate_and_delete_children(parent, data):
for d in deleted_children: for d in deleted_children:
update_bin_on_delete(d, parent.doctype) update_bin_on_delete(d, parent.doctype)
@frappe.whitelist() @frappe.whitelist()
def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, child_docname="items"): def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, child_docname="items"):
def check_doc_permissions(doc, perm_type='create'): def check_doc_permissions(doc, perm_type='create'):

View File

@ -4,6 +4,7 @@
from __future__ import unicode_literals from __future__ import unicode_literals
import frappe import frappe
import erpnext import erpnext
import json
from frappe.desk.reportview import get_match_cond, get_filters_cond from frappe.desk.reportview import get_match_cond, get_filters_cond
from frappe.utils import nowdate, getdate from frappe.utils import nowdate, getdate
from collections import defaultdict from collections import defaultdict
@ -198,6 +199,9 @@ def tax_account_query(doctype, txt, searchfield, start, page_len, filters):
def item_query(doctype, txt, searchfield, start, page_len, filters, as_dict=False): def item_query(doctype, txt, searchfield, start, page_len, filters, as_dict=False):
conditions = [] conditions = []
if isinstance(filters, str):
filters = json.loads(filters)
#Get searchfields from meta and use in Item Link field query #Get searchfields from meta and use in Item Link field query
meta = frappe.get_meta("Item", cached=True) meta = frappe.get_meta("Item", cached=True)
searchfields = meta.get_search_fields() searchfields = meta.get_search_fields()
@ -216,8 +220,9 @@ def item_query(doctype, txt, searchfield, start, page_len, filters, as_dict=Fals
if not field in searchfields] if not field in searchfields]
searchfields = " or ".join([field + " like %(txt)s" for field in searchfields]) searchfields = " or ".join([field + " like %(txt)s" for field in searchfields])
if filters.get('supplier'): if filters and isinstance(filters, dict) and filters.get('supplier'):
item_group_list = frappe.get_all('Supplier Item Group', filters = {'supplier': filters.get('supplier')}, fields = ['item_group']) item_group_list = frappe.get_all('Supplier Item Group',
filters = {'supplier': filters.get('supplier')}, fields = ['item_group'])
item_groups = [] item_groups = []
for i in item_group_list: for i in item_group_list:

View File

@ -1,17 +1,21 @@
# 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
from __future__ import unicode_literals import json
import frappe, erpnext
from frappe.utils import cint, flt, cstr, get_link_to_form, today, getdate
from frappe import _
import frappe.defaults
from collections import defaultdict from collections import defaultdict
from erpnext.accounts.utils import get_fiscal_year, check_if_stock_and_account_balance_synced
import frappe
import frappe.defaults
from frappe import _
from frappe.utils import cint, cstr, flt, get_link_to_form, getdate
import erpnext
from erpnext.accounts.general_ledger import make_gl_entries, make_reverse_gl_entries, process_gl_map from erpnext.accounts.general_ledger import make_gl_entries, make_reverse_gl_entries, process_gl_map
from erpnext.accounts.utils import check_if_stock_and_account_balance_synced, get_fiscal_year
from erpnext.controllers.accounts_controller import AccountsController from erpnext.controllers.accounts_controller import AccountsController
from erpnext.stock.stock_ledger import get_valuation_rate
from erpnext.stock import get_warehouse_account_map from erpnext.stock import get_warehouse_account_map
from erpnext.stock.stock_ledger import get_valuation_rate
class QualityInspectionRequiredError(frappe.ValidationError): pass class QualityInspectionRequiredError(frappe.ValidationError): pass
class QualityInspectionRejectedError(frappe.ValidationError): pass class QualityInspectionRejectedError(frappe.ValidationError): pass
@ -189,7 +193,6 @@ class StockController(AccountsController):
if hasattr(self, "items"): if hasattr(self, "items"):
item_doclist = self.get("items") item_doclist = self.get("items")
elif self.doctype == "Stock Reconciliation": elif self.doctype == "Stock Reconciliation":
import json
item_doclist = [] item_doclist = []
data = json.loads(self.reconciliation_json) data = json.loads(self.reconciliation_json)
for row in data[data.index(self.head_row)+1:]: for row in data[data.index(self.head_row)+1:]:
@ -319,7 +322,7 @@ class StockController(AccountsController):
return serialized_items return serialized_items
def validate_warehouse(self): def validate_warehouse(self):
from erpnext.stock.utils import validate_warehouse_company, validate_disabled_warehouse from erpnext.stock.utils import validate_disabled_warehouse, validate_warehouse_company
warehouses = list(set([d.warehouse for d in warehouses = list(set([d.warehouse for d in
self.get("items") if getattr(d, "warehouse", None)])) self.get("items") if getattr(d, "warehouse", None)]))
@ -498,6 +501,39 @@ class StockController(AccountsController):
check_if_stock_and_account_balance_synced(self.posting_date, check_if_stock_and_account_balance_synced(self.posting_date,
self.company, self.doctype, self.name) self.company, self.doctype, self.name)
@frappe.whitelist()
def make_quality_inspections(doctype, docname, items):
if isinstance(items, str):
items = json.loads(items)
inspections = []
for item in items:
if flt(item.get("sample_size")) > flt(item.get("qty")):
frappe.throw(_("{item_name}'s Sample Size ({sample_size}) cannot be greater than the Accepted Quantity ({accepted_quantity})").format(
item_name=item.get("item_name"),
sample_size=item.get("sample_size"),
accepted_quantity=item.get("qty")
))
quality_inspection = frappe.get_doc({
"doctype": "Quality Inspection",
"inspection_type": "Incoming",
"inspected_by": frappe.session.user,
"reference_type": doctype,
"reference_name": docname,
"item_code": item.get("item_code"),
"description": item.get("description"),
"sample_size": flt(item.get("sample_size")),
"item_serial_no": item.get("serial_no").split("\n")[0] if item.get("serial_no") else None,
"batch_no": item.get("batch_no")
}).insert()
quality_inspection.save()
inspections.append(quality_inspection.name)
return inspections
def is_reposting_pending(): def is_reposting_pending():
return frappe.db.exists("Repost Item Valuation", return frappe.db.exists("Repost Item Valuation",
{'docstatus': 1, 'status': ['in', ['Queued','In Progress']]}) {'docstatus': 1, 'status': ['in', ['Queued','In Progress']]})

View File

@ -280,7 +280,6 @@
"read_only": 1 "read_only": 1
}, },
{ {
"depends_on": "eval:",
"fieldname": "territory", "fieldname": "territory",
"fieldtype": "Link", "fieldtype": "Link",
"label": "Territory", "label": "Territory",
@ -431,7 +430,7 @@
"icon": "fa fa-info-sign", "icon": "fa fa-info-sign",
"idx": 195, "idx": 195,
"links": [], "links": [],
"modified": "2021-01-06 19:42:46.190051", "modified": "2021-06-04 10:11:22.831139",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "CRM", "module": "CRM",
"name": "Opportunity", "name": "Opportunity",

View File

@ -9,8 +9,7 @@ from frappe.utils import nowdate
from frappe.utils.make_random import get_random from frappe.utils.make_random import get_random
from erpnext.education.doctype.program.test_program import make_program_and_linked_courses from erpnext.education.doctype.program.test_program import make_program_and_linked_courses
# test_records = frappe.get_test_records('Fees') test_dependencies = ['Company']
class TestFees(unittest.TestCase): class TestFees(unittest.TestCase):
def test_fees(self): def test_fees(self):

View File

@ -219,7 +219,6 @@ def get_quiz(quiz_name, course):
try: try:
quiz = frappe.get_doc("Quiz", quiz_name) quiz = frappe.get_doc("Quiz", quiz_name)
questions = quiz.get_questions() questions = quiz.get_questions()
duration = quiz.duration
except: except:
frappe.throw(_("Quiz {0} does not exist").format(quiz_name), frappe.DoesNotExistError) frappe.throw(_("Quiz {0} does not exist").format(quiz_name), frappe.DoesNotExistError)
return None return None
@ -236,7 +235,8 @@ def get_quiz(quiz_name, course):
return { return {
'questions': questions, 'questions': questions,
'activity': None, 'activity': None,
'duration':duration 'is_time_bound': quiz.is_time_bound,
'duration': quiz.duration
} }
student = get_current_student() student = get_current_student()
@ -245,6 +245,7 @@ def get_quiz(quiz_name, course):
return { return {
'questions': questions, 'questions': questions,
'activity': {'is_complete': status, 'score': score, 'result': result, 'time_taken': time_taken}, 'activity': {'is_complete': status, 'score': score, 'result': result, 'time_taken': time_taken},
'is_time_bound': quiz.is_time_bound,
'duration': quiz.duration 'duration': quiz.duration
} }

View File

@ -99,5 +99,7 @@ class PlaidConnector():
response = self.client.Transactions.get(self.access_token, start_date=start_date, end_date=end_date, offset=len(transactions)) response = self.client.Transactions.get(self.access_token, start_date=start_date, end_date=end_date, offset=len(transactions))
transactions.extend(response["transactions"]) transactions.extend(response["transactions"])
return transactions return transactions
except ItemError as e:
raise e
except Exception: except Exception:
frappe.log_error(frappe.get_traceback(), _("Plaid transactions sync error")) frappe.log_error(frappe.get_traceback(), _("Plaid transactions sync error"))

View File

@ -16,6 +16,10 @@ frappe.ui.form.on('Plaid Settings', {
new erpnext.integrations.plaidLink(frm); new erpnext.integrations.plaidLink(frm);
}); });
frm.add_custom_button(__('Reset Plaid Link'), () => {
new erpnext.integrations.plaidLink(frm);
});
frm.add_custom_button(__("Sync Now"), () => { frm.add_custom_button(__("Sync Now"), () => {
frappe.call({ frappe.call({
method: "erpnext.erpnext_integrations.doctype.plaid_settings.plaid_settings.enqueue_synchronization", method: "erpnext.erpnext_integrations.doctype.plaid_settings.plaid_settings.enqueue_synchronization",

View File

@ -12,6 +12,7 @@ from frappe.desk.doctype.tag.tag import add_tag
from frappe.model.document import Document from frappe.model.document import Document
from frappe.utils import add_months, formatdate, getdate, today from frappe.utils import add_months, formatdate, getdate, today
from plaid.errors import ItemError
class PlaidSettings(Document): class PlaidSettings(Document):
@staticmethod @staticmethod
@ -51,7 +52,7 @@ def add_institution(token, response):
}) })
bank.insert() bank.insert()
except Exception: except Exception:
frappe.throw(frappe.get_traceback()) frappe.log_error(frappe.get_traceback(), title=_('Plaid Link Error'))
else: else:
bank = frappe.get_doc("Bank", response["institution"]["name"]) bank = frappe.get_doc("Bank", response["institution"]["name"])
bank.plaid_access_token = access_token bank.plaid_access_token = access_token
@ -83,7 +84,12 @@ def add_bank_accounts(response, bank, company):
if not acc_subtype: if not acc_subtype:
add_account_subtype(account["subtype"]) add_account_subtype(account["subtype"])
if not frappe.db.exists("Bank Account", dict(integration_id=account["id"])): existing_bank_account = frappe.db.exists("Bank Account", {
'account_name': account["name"],
'bank': bank["bank_name"]
})
if not existing_bank_account:
try: try:
new_account = frappe.get_doc({ new_account = frappe.get_doc({
"doctype": "Bank Account", "doctype": "Bank Account",
@ -103,10 +109,27 @@ def add_bank_accounts(response, bank, company):
except frappe.UniqueValidationError: except frappe.UniqueValidationError:
frappe.msgprint(_("Bank account {0} already exists and could not be created again").format(account["name"])) frappe.msgprint(_("Bank account {0} already exists and could not be created again").format(account["name"]))
except Exception: except Exception:
frappe.throw(frappe.get_traceback()) frappe.log_error(frappe.get_traceback(), title=_("Plaid Link Error"))
frappe.throw(_("There was an error creating Bank Account while linking with Plaid."),
title=_("Plaid Link Failed"))
else: else:
result.append(frappe.db.get_value("Bank Account", dict(integration_id=account["id"]), "name")) try:
existing_account = frappe.get_doc('Bank Account', existing_bank_account)
existing_account.update({
"bank": bank["bank_name"],
"account_name": account["name"],
"account_type": account.get("type", ""),
"account_subtype": account.get("subtype", ""),
"mask": account.get("mask", ""),
"integration_id": account["id"]
})
existing_account.save()
result.append(existing_bank_account)
except Exception:
frappe.log_error(frappe.get_traceback(), title=_("Plaid Link Error"))
frappe.throw(_("There was an error updating Bank Account {} while linking with Plaid.").format(
existing_bank_account), title=_("Plaid Link Failed"))
return result return result
@ -172,9 +195,16 @@ def get_transactions(bank, bank_account=None, start_date=None, end_date=None):
account_id = None account_id = None
plaid = PlaidConnector(access_token) plaid = PlaidConnector(access_token)
transactions = plaid.get_transactions(start_date=start_date, end_date=end_date, account_id=account_id)
return transactions try:
transactions = plaid.get_transactions(start_date=start_date, end_date=end_date, account_id=account_id)
except ItemError as e:
if e.code == "ITEM_LOGIN_REQUIRED":
msg = _("There was an error syncing transactions.") + " "
msg += _("Please refresh or reset the Plaid linking of the Bank {}.").format(bank) + " "
frappe.log_error(msg, title=_("Plaid Link Refresh Required"))
return transactions or []
def new_bank_transaction(transaction): def new_bank_transaction(transaction):

View File

@ -332,7 +332,9 @@ scheduler_events = {
"erpnext.projects.doctype.project.project.collect_project_status", "erpnext.projects.doctype.project.project.collect_project_status",
"erpnext.hr.doctype.shift_type.shift_type.process_auto_attendance_for_all_shifts", "erpnext.hr.doctype.shift_type.shift_type.process_auto_attendance_for_all_shifts",
"erpnext.support.doctype.issue.issue.set_service_level_agreement_variance", "erpnext.support.doctype.issue.issue.set_service_level_agreement_variance",
"erpnext.erpnext_integrations.connectors.shopify_connection.sync_old_orders", "erpnext.erpnext_integrations.connectors.shopify_connection.sync_old_orders"
],
"hourly_long": [
"erpnext.stock.doctype.repost_item_valuation.repost_item_valuation.repost_entries" "erpnext.stock.doctype.repost_item_valuation.repost_item_valuation.repost_entries"
], ],
"daily": [ "daily": [

View File

@ -4,8 +4,7 @@
from __future__ import unicode_literals from __future__ import unicode_literals
import frappe import frappe
from frappe import _ from frappe import _
from frappe.utils import cint, cstr, date_diff, flt, formatdate, getdate, get_link_to_form, \ from frappe.utils import cint, cstr, date_diff, flt, formatdate, getdate, get_link_to_form, get_fullname, add_days, nowdate
comma_or, get_fullname, add_days, nowdate, get_datetime_str
from erpnext.hr.utils import set_employee_name, get_leave_period, share_doc_with_approver from erpnext.hr.utils import set_employee_name, get_leave_period, share_doc_with_approver
from erpnext.hr.doctype.leave_block_list.leave_block_list import get_applicable_block_dates from erpnext.hr.doctype.leave_block_list.leave_block_list import get_applicable_block_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
@ -85,7 +84,7 @@ class LeaveApplication(Document):
def validate_dates(self): def validate_dates(self):
if frappe.db.get_single_value("HR Settings", "restrict_backdated_leave_application"): if frappe.db.get_single_value("HR Settings", "restrict_backdated_leave_application"):
if self.from_date and self.from_date < frappe.utils.today(): if self.from_date and getdate(self.from_date) < getdate():
allowed_role = frappe.db.get_single_value("HR Settings", "role_allowed_to_create_backdated_leave_application") allowed_role = frappe.db.get_single_value("HR Settings", "role_allowed_to_create_backdated_leave_application")
if allowed_role not in frappe.get_roles(): if allowed_role not in frappe.get_roles():
frappe.throw(_("Only users with the {0} role can create backdated leave applications").format(allowed_role)) frappe.throw(_("Only users with the {0} role can create backdated leave applications").format(allowed_role))
@ -248,9 +247,9 @@ class LeaveApplication(Document):
self.throw_overlap_error(d) self.throw_overlap_error(d)
def throw_overlap_error(self, d): def throw_overlap_error(self, d):
msg = _("Employee {0} has already applied for {1} between {2} and {3} : ").format(self.employee, form_link = get_link_to_form("Leave Application", d.name)
d['leave_type'], formatdate(d['from_date']), formatdate(d['to_date'])) \ msg = _("Employee {0} has already applied for {1} between {2} and {3} : {4}").format(self.employee,
+ """ <b><a href="/app/Form/Leave Application/{0}">{0}</a></b>""".format(d["name"]) d['leave_type'], formatdate(d['from_date']), formatdate(d['to_date']), form_link)
frappe.throw(msg, OverlapError) frappe.throw(msg, OverlapError)
def get_total_leaves_on_half_day(self): def get_total_leaves_on_half_day(self):
@ -356,7 +355,7 @@ class LeaveApplication(Document):
sender = dict() sender = dict()
sender['email'] = frappe.get_doc('User', frappe.session.user).email sender['email'] = frappe.get_doc('User', frappe.session.user).email
sender['full_name'] = frappe.utils.get_fullname(sender['email']) sender['full_name'] = get_fullname(sender['email'])
try: try:
frappe.sendmail( frappe.sendmail(

View File

@ -5,11 +5,18 @@ from __future__ import unicode_literals
import frappe import frappe
import unittest import unittest
import erpnext
from frappe.utils import getdate from frappe.utils import getdate
from erpnext.hr.doctype.upload_attendance.upload_attendance import get_data from erpnext.hr.doctype.upload_attendance.upload_attendance import get_data
from erpnext.hr.doctype.employee.test_employee import make_employee from erpnext.hr.doctype.employee.test_employee import make_employee
test_dependencies = ['Holiday List']
class TestUploadAttendance(unittest.TestCase): class TestUploadAttendance(unittest.TestCase):
@classmethod
def setUpClass(cls):
frappe.db.set_value("Company", erpnext.get_default_company(), "default_holiday_list", '_Test Holiday List')
def test_date_range(self): def test_date_range(self):
employee = make_employee("test_employee@company.com") employee = make_employee("test_employee@company.com")
employee_doc = frappe.get_doc("Employee", employee) employee_doc = frappe.get_doc("Employee", employee)

View File

@ -24,17 +24,7 @@ class TestVehicleLog(unittest.TestCase):
frappe.delete_doc("Employee", self.employee_id, force=1) frappe.delete_doc("Employee", self.employee_id, force=1)
def test_make_vehicle_log_and_syncing_of_odometer_value(self): def test_make_vehicle_log_and_syncing_of_odometer_value(self):
vehicle_log = frappe.get_doc({ vehicle_log = make_vehicle_log(self.license_plate, self.employee_id)
"doctype": "Vehicle Log",
"license_plate": cstr(self.license_plate),
"employee": self.employee_id,
"date":frappe.utils.nowdate(),
"odometer":5010,
"fuel_qty":frappe.utils.flt(50),
"price": frappe.utils.flt(500)
})
vehicle_log.save()
vehicle_log.submit()
#checking value of vehicle odometer value on submit. #checking value of vehicle odometer value on submit.
vehicle = frappe.get_doc("Vehicle", self.license_plate) vehicle = frappe.get_doc("Vehicle", self.license_plate)
@ -53,17 +43,7 @@ class TestVehicleLog(unittest.TestCase):
vehicle_log.delete() vehicle_log.delete()
def test_vehicle_log_fuel_expense(self): def test_vehicle_log_fuel_expense(self):
vehicle_log = frappe.get_doc({ vehicle_log = make_vehicle_log(self.license_plate, self.employee_id)
"doctype": "Vehicle Log",
"license_plate": cstr(self.license_plate),
"employee": self.employee_id,
"date": frappe.utils.nowdate(),
"odometer":5010,
"fuel_qty":frappe.utils.flt(50),
"price": frappe.utils.flt(500)
})
vehicle_log.save()
vehicle_log.submit()
expense_claim = make_expense_claim(vehicle_log.name) expense_claim = make_expense_claim(vehicle_log.name)
fuel_expense = expense_claim.expenses[0].amount fuel_expense = expense_claim.expenses[0].amount
@ -73,6 +53,18 @@ class TestVehicleLog(unittest.TestCase):
frappe.delete_doc("Expense Claim", expense_claim.name) frappe.delete_doc("Expense Claim", expense_claim.name)
frappe.delete_doc("Vehicle Log", vehicle_log.name) frappe.delete_doc("Vehicle Log", vehicle_log.name)
def test_vehicle_log_with_service_expenses(self):
vehicle_log = make_vehicle_log(self.license_plate, self.employee_id, with_services=True)
expense_claim = make_expense_claim(vehicle_log.name)
expenses = expense_claim.expenses[0].amount
self.assertEqual(expenses, 27000)
vehicle_log.cancel()
frappe.delete_doc("Expense Claim", expense_claim.name)
frappe.delete_doc("Vehicle Log", vehicle_log.name)
def get_vehicle(employee_id): def get_vehicle(employee_id):
license_plate=random_string(10).upper() license_plate=random_string(10).upper()
vehicle = frappe.get_doc({ vehicle = frappe.get_doc({
@ -82,14 +74,45 @@ def get_vehicle(employee_id):
"model": "PCM", "model": "PCM",
"employee": employee_id, "employee": employee_id,
"last_odometer": 5000, "last_odometer": 5000,
"acquisition_date":frappe.utils.nowdate(), "acquisition_date": nowdate(),
"location": "Mumbai", "location": "Mumbai",
"chassis_no": "1234ABCD", "chassis_no": "1234ABCD",
"uom": "Litre", "uom": "Litre",
"vehicle_value":frappe.utils.flt(500000) "vehicle_value": flt(500000)
}) })
try: try:
vehicle.insert() vehicle.insert()
except frappe.DuplicateEntryError: except frappe.DuplicateEntryError:
pass pass
return license_plate return license_plate
def make_vehicle_log(license_plate, employee_id, with_services=False):
vehicle_log = frappe.get_doc({
"doctype": "Vehicle Log",
"license_plate": cstr(license_plate),
"employee": employee_id,
"date": nowdate(),
"odometer": 5010,
"fuel_qty": flt(50),
"price": flt(500)
})
if with_services:
vehicle_log.append("service_detail", {
"service_item": "Oil Change",
"type": "Inspection",
"frequency": "Mileage",
"expense_amount": flt(500)
})
vehicle_log.append("service_detail", {
"service_item": "Wheels",
"type": "Change",
"frequency": "Half Yearly",
"expense_amount": flt(1500)
})
vehicle_log.save()
vehicle_log.submit()
return vehicle_log

View File

@ -1,4 +1,5 @@
{ {
"actions": [],
"autoname": "naming_series:", "autoname": "naming_series:",
"creation": "2016-09-03 14:14:51.788550", "creation": "2016-09-03 14:14:51.788550",
"doctype": "DocType", "doctype": "DocType",
@ -10,7 +11,6 @@
"naming_series", "naming_series",
"license_plate", "license_plate",
"employee", "employee",
"column_break_4",
"column_break_7", "column_break_7",
"model", "model",
"make", "make",
@ -65,10 +65,6 @@
"options": "Employee", "options": "Employee",
"reqd": 1 "reqd": 1
}, },
{
"fieldname": "column_break_4",
"fieldtype": "Column Break"
},
{ {
"fieldname": "column_break_7", "fieldname": "column_break_7",
"fieldtype": "Column Break" "fieldtype": "Column Break"
@ -142,7 +138,6 @@
{ {
"fieldname": "service_detail", "fieldname": "service_detail",
"fieldtype": "Table", "fieldtype": "Table",
"label": "Service Detail",
"options": "Vehicle Service" "options": "Vehicle Service"
}, },
{ {
@ -158,7 +153,7 @@
"fetch_from": "license_plate.last_odometer", "fetch_from": "license_plate.last_odometer",
"fieldname": "last_odometer", "fieldname": "last_odometer",
"fieldtype": "Int", "fieldtype": "Int",
"label": "last Odometer Value ", "label": "Last Odometer Value ",
"read_only": 1, "read_only": 1,
"reqd": 1 "reqd": 1
}, },
@ -168,7 +163,8 @@
} }
], ],
"is_submittable": 1, "is_submittable": 1,
"modified": "2020-03-18 16:45:45.060761", "links": [],
"modified": "2021-05-17 00:10:21.188352",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "HR", "module": "HR",
"name": "Vehicle Log", "name": "Vehicle Log",

View File

@ -37,5 +37,22 @@ frappe.query_reports["Employee Leave Balance"] = {
"fieldtype": "Link", "fieldtype": "Link",
"options": "Employee", "options": "Employee",
} }
] ],
onload: () => {
frappe.call({
type: "GET",
method: "erpnext.hr.utils.get_leave_period",
args: {
"from_date": frappe.defaults.get_default("year_start_date"),
"to_date": frappe.defaults.get_default("year_end_date"),
"company": frappe.defaults.get_user_default("Company")
},
freeze: true,
callback: (data) => {
frappe.query_report.set_filter_value("from_date", data.message[0].from_date);
frappe.query_report.set_filter_value("to_date", data.message[0].to_date);
}
});
}
} }

View File

@ -6,15 +6,16 @@ import frappe
from frappe.utils import flt, add_days from frappe.utils import flt, add_days
from frappe import _ from frappe import _
from erpnext.hr.doctype.leave_application.leave_application import get_leaves_for_period, get_leave_balance_on from erpnext.hr.doctype.leave_application.leave_application import get_leaves_for_period, get_leave_balance_on
from itertools import groupby
def execute(filters=None): def execute(filters=None):
if filters.to_date <= filters.from_date: if filters.to_date <= filters.from_date:
frappe.throw(_('"From date" can not be greater than or equal to "To date"')) frappe.throw(_('"From Date" can not be greater than or equal to "To Date"'))
columns = get_columns() columns = get_columns()
data = get_data(filters) data = get_data(filters)
charts = get_chart_data(data)
return columns, data return columns, data, None, charts
def get_columns(): def get_columns():
columns = [{ columns = [{
@ -31,9 +32,10 @@ def get_columns():
'options': 'Employee' 'options': 'Employee'
}, { }, {
'label': _('Employee Name'), 'label': _('Employee Name'),
'fieldtype': 'Data', 'fieldtype': 'Dynamic Link',
'fieldname': 'employee_name', 'fieldname': 'employee_name',
'width': 100, 'width': 100,
'options': 'employee'
}, { }, {
'label': _('Opening Balance'), 'label': _('Opening Balance'),
'fieldtype': 'float', 'fieldtype': 'float',
@ -64,8 +66,7 @@ def get_columns():
return columns return columns
def get_data(filters): def get_data(filters):
leave_types = frappe.db.sql_list("SELECT `name` FROM `tabLeave Type` ORDER BY `name` ASC") leave_types = frappe.db.get_list('Leave Type', pluck='name', order_by='name')
conditions = get_conditions(filters) conditions = get_conditions(filters)
user = frappe.session.user user = frappe.session.user
@ -113,12 +114,8 @@ def get_data(filters):
# not be shown on the basis of days left it create in user mind for carry_forward leave # not be shown on the basis of days left it create in user mind for carry_forward leave
row.closing_balance = (new_allocation + opening - (row.leaves_expired + leaves_taken)) row.closing_balance = (new_allocation + opening - (row.leaves_expired + leaves_taken))
row.indent = 1 row.indent = 1
data.append(row) data.append(row)
new_leaves_allocated = 0
return data return data
@ -129,27 +126,37 @@ def get_conditions(filters):
if filters.get('employee'): if filters.get('employee'):
conditions['name'] = filters.get('employee') conditions['name'] = filters.get('employee')
if filters.get('employee'):
conditions['name'] = filters.get('employee')
if filters.get('company'): if filters.get('company'):
conditions['company'] = filters.get('company') conditions['company'] = filters.get('company')
if filters.get('department'):
conditions['department'] = filters.get('department')
return conditions return conditions
def get_department_leave_approver_map(department=None): def get_department_leave_approver_map(department=None):
conditions=''
if department:
conditions="and (department_name = '%(department)s' or parent_department = '%(department)s')"%{'department': department}
# get current department and all its child # get current department and all its child
department_list = frappe.db.sql_list(""" SELECT name FROM `tabDepartment` WHERE disabled=0 {0}""".format(conditions)) #nosec department_list = frappe.get_list('Department',
filters={
'disabled': 0
},
or_filters={
'name': department,
'parent_department': department
},
fields=['name'],
pluck='name'
)
# retrieve approvers list from current department and from its subsequent child departments # retrieve approvers list from current department and from its subsequent child departments
approver_list = frappe.get_all('Department Approver', filters={ approver_list = frappe.get_all('Department Approver',
filters={
'parentfield': 'leave_approvers', 'parentfield': 'leave_approvers',
'parent': ('in', department_list) 'parent': ('in', department_list)
}, fields=['parent', 'approver'], as_list=1) },
fields=['parent', 'approver'],
as_list=1
)
approvers = {} approvers = {}
@ -190,3 +197,40 @@ def get_allocated_and_expired_leaves(from_date, to_date, employee, leave_type):
new_allocation += record.leaves new_allocation += record.leaves
return new_allocation, expired_leaves return new_allocation, expired_leaves
def get_chart_data(data):
labels = []
datasets = []
employee_data = data
if data and data[0].get('employee_name'):
get_dataset_for_chart(employee_data, datasets, labels)
chart = {
'data': {
'labels': labels,
'datasets': datasets
},
'type': 'bar',
'colors': ['#456789', '#EE8888', '#7E77BF']
}
return chart
def get_dataset_for_chart(employee_data, datasets, labels):
leaves = []
employee_data = sorted(employee_data, key=lambda k: k['employee_name'])
for key, group in groupby(employee_data, lambda x: x['employee_name']):
for grp in group:
if grp.closing_balance:
leaves.append(frappe._dict({
'leave_type': grp.leave_type,
'closing_balance': grp.closing_balance
}))
if leaves:
labels.append(key)
for leave in leaves:
datasets.append({'name': leave.leave_type, 'values': [leave.closing_balance]})

View File

@ -0,0 +1,73 @@
# Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and Contributors
# License: GNU General Public License v3. See license.txt
from __future__ import unicode_literals
import unittest
import frappe
from frappe.utils import getdate
from erpnext.hr.doctype.employee.test_employee import make_employee
from erpnext.hr.doctype.vehicle_log.vehicle_log import make_expense_claim
from erpnext.hr.doctype.vehicle_log.test_vehicle_log import get_vehicle, make_vehicle_log
from erpnext.hr.report.vehicle_expenses.vehicle_expenses import execute
from erpnext.accounts.utils import get_fiscal_year
class TestVehicleExpenses(unittest.TestCase):
@classmethod
def setUpClass(self):
frappe.db.sql('delete from `tabVehicle Log`')
employee_id = frappe.db.sql('''select name from `tabEmployee` where name="testdriver@example.com"''')
self.employee_id = employee_id[0][0] if employee_id else None
if not self.employee_id:
self.employee_id = make_employee('testdriver@example.com', company='_Test Company')
self.license_plate = get_vehicle(self.employee_id)
def test_vehicle_expenses_based_on_fiscal_year(self):
vehicle_log = make_vehicle_log(self.license_plate, self.employee_id, with_services=True)
expense_claim = make_expense_claim(vehicle_log.name)
# Based on Fiscal Year
filters = {
'filter_based_on': 'Fiscal Year',
'fiscal_year': get_fiscal_year(getdate())[0]
}
report = execute(filters)
expected_data = [{
'vehicle': self.license_plate,
'make': 'Maruti',
'model': 'PCM',
'location': 'Mumbai',
'log_name': vehicle_log.name,
'odometer': 5010,
'date': getdate(),
'fuel_qty': 50.0,
'fuel_price': 500.0,
'fuel_expense': 25000.0,
'service_expense': 2000.0,
'employee': self.employee_id
}]
self.assertEqual(report[1], expected_data)
# Based on Date Range
fiscal_year = get_fiscal_year(getdate(), as_dict=True)
filters = {
'filter_based_on': 'Date Range',
'from_date': fiscal_year.year_start_date,
'to_date': fiscal_year.year_end_date
}
report = execute(filters)
self.assertEqual(report[1], expected_data)
# clean up
vehicle_log.cancel()
frappe.delete_doc('Expense Claim', expense_claim.name)
frappe.delete_doc('Vehicle Log', vehicle_log.name)
def tearDown(self):
frappe.delete_doc('Vehicle', self.license_plate, force=1)
frappe.delete_doc('Employee', self.employee_id, force=1)

View File

@ -1,31 +1,52 @@
// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors // Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt // For license information, please see license.txt
frappe.require("assets/erpnext/js/financial_statements.js", function() {
frappe.query_reports["Vehicle Expenses"] = { frappe.query_reports["Vehicle Expenses"] = {
"filters": [ "filters": [
{
"fieldname": "filter_based_on",
"label": __("Filter Based On"),
"fieldtype": "Select",
"options": ["Fiscal Year", "Date Range"],
"default": ["Fiscal Year"],
"reqd": 1
},
{ {
"fieldname": "fiscal_year", "fieldname": "fiscal_year",
"label": __("Fiscal Year"), "label": __("Fiscal Year"),
"fieldtype": "Link", "fieldtype": "Link",
"options": "Fiscal Year", "options": "Fiscal Year",
"default": frappe.defaults.get_user_default("fiscal_year"), "default": frappe.defaults.get_user_default("fiscal_year"),
"depends_on": "eval: doc.filter_based_on == 'Fiscal Year'",
"reqd": 1
},
{
"fieldname": "from_date",
"label": __("From Date"),
"fieldtype": "Date",
"reqd": 1, "reqd": 1,
"on_change": function(query_report) { "depends_on": "eval: doc.filter_based_on == 'Date Range'",
var fiscal_year = query_report.get_values().fiscal_year; "default": frappe.datetime.add_months(frappe.datetime.nowdate(), -12)
if (!fiscal_year) { },
return; {
} "fieldname": "to_date",
frappe.model.with_doc("Fiscal Year", fiscal_year, function(r) { "label": __("To Date"),
var fy = frappe.model.get_doc("Fiscal Year", fiscal_year); "fieldtype": "Date",
"reqd": 1,
frappe.query_report.set_filter({ "depends_on": "eval: doc.filter_based_on == 'Date Range'",
from_date: fy.year_start_date, "default": frappe.datetime.nowdate()
to_date: fy.year_end_date },
}); {
}); "fieldname": "vehicle",
} "label": __("Vehicle"),
"fieldtype": "Link",
"options": "Vehicle"
},
{
"fieldname": "employee",
"label": __("Employee"),
"fieldtype": "Link",
"options": "Employee"
} }
] ]
} };
});

View File

@ -1,17 +1,20 @@
{ {
"add_total_row": 0, "add_total_row": 1,
"apply_user_permissions": 1, "columns": [],
"creation": "2016-09-09 03:33:40.605734", "creation": "2016-09-09 03:33:40.605734",
"disable_prepared_report": 0,
"disabled": 0, "disabled": 0,
"docstatus": 0, "docstatus": 0,
"doctype": "Report", "doctype": "Report",
"filters": [],
"idx": 2, "idx": 2,
"is_standard": "Yes", "is_standard": "Yes",
"modified": "2017-02-24 19:59:18.641284", "modified": "2021-05-16 22:48:22.767535",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "HR", "module": "HR",
"name": "Vehicle Expenses", "name": "Vehicle Expenses",
"owner": "Administrator", "owner": "Administrator",
"prepared_report": 0,
"ref_doctype": "Vehicle", "ref_doctype": "Vehicle",
"report_name": "Vehicle Expenses", "report_name": "Vehicle Expenses",
"report_type": "Script Report", "report_type": "Script Report",

View File

@ -5,86 +5,209 @@ from __future__ import unicode_literals
import frappe import frappe
import erpnext import erpnext
from frappe import _ from frappe import _
from frappe.utils import flt,cstr from frappe.utils import flt
from erpnext.accounts.report.financial_statements import get_period_list from erpnext.accounts.report.financial_statements import get_period_list
def execute(filters=None): def execute(filters=None):
columns, data, chart = [], [], [] filters = frappe._dict(filters or {})
if filters.get('fiscal_year'):
company = erpnext.get_default_company()
period_list = get_period_list(filters.get('fiscal_year'), filters.get('fiscal_year'),
'', '', 'Fiscal Year', 'Monthly', company=company)
columns = get_columns() columns = get_columns()
data=get_log_data(filters) data = get_vehicle_log_data(filters)
chart=get_chart_data(data,period_list) chart = get_chart_data(data, filters)
return columns, data, None, chart return columns, data, None, chart
def get_columns(): def get_columns():
columns = [_("License") + ":Link/Vehicle:100", _('Create') + ":data:50", return [
_("Model") + ":data:50", _("Location") + ":data:100", {
_("Log") + ":Link/Vehicle Log:100", _("Odometer") + ":Int:80", 'fieldname': 'vehicle',
_("Date") + ":Date:100", _("Fuel Qty") + ":Float:80", 'fieldtype': 'Link',
_("Fuel Price") + ":Float:100",_("Fuel Expense") + ":Float:100", 'label': _('Vehicle'),
_("Service Expense") + ":Float:100" 'options': 'Vehicle',
'width': 150
},
{
'fieldname': 'make',
'fieldtype': 'Data',
'label': _('Make'),
'width': 100
},
{
'fieldname': 'model',
'fieldtype': 'Data',
'label': _('Model'),
'width': 80
},
{
'fieldname': 'location',
'fieldtype': 'Data',
'label': _('Location'),
'width': 100
},
{
'fieldname': 'log_name',
'fieldtype': 'Link',
'label': _('Vehicle Log'),
'options': 'Vehicle Log',
'width': 100
},
{
'fieldname': 'odometer',
'fieldtype': 'Int',
'label': _('Odometer Value'),
'width': 120
},
{
'fieldname': 'date',
'fieldtype': 'Date',
'label': _('Date'),
'width': 100
},
{
'fieldname': 'fuel_qty',
'fieldtype': 'Float',
'label': _('Fuel Qty'),
'width': 80
},
{
'fieldname': 'fuel_price',
'fieldtype': 'Float',
'label': _('Fuel Price'),
'width': 100
},
{
'fieldname': 'fuel_expense',
'fieldtype': 'Currency',
'label': _('Fuel Expense'),
'width': 150
},
{
'fieldname': 'service_expense',
'fieldtype': 'Currency',
'label': _('Service Expense'),
'width': 150
},
{
'fieldname': 'employee',
'fieldtype': 'Link',
'label': _('Employee'),
'options': 'Employee',
'width': 150
}
] ]
return columns return columns
def get_log_data(filters):
fy = frappe.db.get_value('Fiscal Year', filters.get('fiscal_year'), ['year_start_date', 'year_end_date'], as_dict=True) def get_vehicle_log_data(filters):
data = frappe.db.sql("""select start_date, end_date = get_period_dates(filters)
vhcl.license_plate as "License", vhcl.make as "Make", vhcl.model as "Model", conditions, values = get_conditions(filters)
vhcl.location as "Location", log.name as "Log", log.odometer as "Odometer",
log.date as "Date", log.fuel_qty as "Fuel Qty", log.price as "Fuel Price", data = frappe.db.sql("""
log.fuel_qty * log.price as "Fuel Expense" SELECT
from vhcl.license_plate as vehicle, vhcl.make, vhcl.model,
vhcl.location, log.name as log_name, log.odometer,
log.date, log.employee, log.fuel_qty,
log.price as fuel_price,
log.fuel_qty * log.price as fuel_expense
FROM
`tabVehicle` vhcl,`tabVehicle Log` log `tabVehicle` vhcl,`tabVehicle Log` log
where WHERE
vhcl.license_plate = log.license_plate and log.docstatus = 1 and date between %s and %s vhcl.license_plate = log.license_plate
order by date""" ,(fy.year_start_date, fy.year_end_date), as_dict=1) and log.docstatus = 1
dl=list(data) and date between %(start_date)s and %(end_date)s
for row in dl: {0}
row["Service Expense"]= get_service_expense(row["Log"]) ORDER BY date""".format(conditions), values, as_dict=1)
return dl
for row in data:
row['service_expense'] = get_service_expense(row.log_name)
return data
def get_conditions(filters):
conditions = ''
start_date, end_date = get_period_dates(filters)
values = {
'start_date': start_date,
'end_date': end_date
}
if filters.employee:
conditions += ' and log.employee = %(employee)s'
values['employee'] = filters.employee
if filters.vehicle:
conditions += ' and vhcl.license_plate = %(vehicle)s'
values['vehicle'] = filters.vehicle
return conditions, values
def get_period_dates(filters):
if filters.filter_based_on == 'Fiscal Year' and filters.fiscal_year:
fy = frappe.db.get_value('Fiscal Year', filters.fiscal_year,
['year_start_date', 'year_end_date'], as_dict=True)
return fy.year_start_date, fy.year_end_date
else:
return filters.from_date, filters.to_date
def get_service_expense(logname): def get_service_expense(logname):
expense_amount = frappe.db.sql("""select sum(expense_amount) expense_amount = frappe.db.sql("""
from `tabVehicle Log` log,`tabVehicle Service` ser SELECT sum(expense_amount)
where ser.parent=log.name and log.name=%s""",logname) FROM
return flt(expense_amount[0][0]) if expense_amount else 0 `tabVehicle Log` log, `tabVehicle Service` service
WHERE
service.parent=log.name and log.name=%s
""", logname)
return flt(expense_amount[0][0]) if expense_amount else 0.0
def get_chart_data(data, filters):
period_list = get_period_list(filters.fiscal_year, filters.fiscal_year,
filters.from_date, filters.to_date, filters.filter_based_on, 'Monthly')
fuel_data, service_data = [], []
def get_chart_data(data,period_list):
fuel_exp_data,service_exp_data,fueldata,servicedata = [],[],[],[]
service_exp_data = []
fueldata = []
for period in period_list: for period in period_list:
total_fuel_exp = 0 total_fuel_exp = 0
total_ser_exp=0 total_service_exp = 0
for row in data:
if row["Date"] <= period.to_date and row["Date"] >= period.from_date: for row in data:
total_fuel_exp+=flt(row["Fuel Expense"]) if row.date <= period.to_date and row.date >= period.from_date:
total_ser_exp+=flt(row["Service Expense"]) total_fuel_exp += flt(row.fuel_expense)
fueldata.append([period.key,total_fuel_exp]) total_service_exp += flt(row.service_expense)
servicedata.append([period.key,total_ser_exp])
fuel_data.append([period.key, total_fuel_exp])
service_data.append([period.key, total_service_exp])
labels = [period.label for period in period_list]
fuel_exp_data= [row[1] for row in fuel_data]
service_exp_data= [row[1] for row in service_data]
labels = [period.key for period in period_list]
fuel_exp_data= [row[1] for row in fueldata]
service_exp_data= [row[1] for row in servicedata]
datasets = [] datasets = []
if fuel_exp_data: if fuel_exp_data:
datasets.append({ datasets.append({
'name': 'Fuel Expenses', 'name': _('Fuel Expenses'),
'values': fuel_exp_data 'values': fuel_exp_data
}) })
if service_exp_data: if service_exp_data:
datasets.append({ datasets.append({
'name': 'Service Expenses', 'name': _('Service Expenses'),
'values': service_exp_data 'values': service_exp_data
}) })
chart = { chart = {
"data": { 'data': {
'labels': labels, 'labels': labels,
'datasets': datasets 'datasets': datasets
},
'type': 'line',
'fieldtype': 'Currency'
} }
}
chart["type"] = "line"
return chart return chart

View File

@ -269,6 +269,7 @@ def get_total_exemption_amount(declarations):
total_exemption_amount = sum([flt(d.total_exemption_amount) for d in exemptions.values()]) total_exemption_amount = sum([flt(d.total_exemption_amount) for d in exemptions.values()])
return total_exemption_amount return total_exemption_amount
@frappe.whitelist()
def get_leave_period(from_date, to_date, company): def get_leave_period(from_date, to_date, company):
leave_period = frappe.db.sql(""" leave_period = frappe.db.sql("""
select name, from_date, to_date select name, from_date, to_date

View File

@ -12,7 +12,7 @@
"idx": 0, "idx": 0,
"is_default": 0, "is_default": 0,
"is_standard": 1, "is_standard": 1,
"label": "Loan Management", "label": "Loans",
"links": [ "links": [
{ {
"hidden": 0, "hidden": 0,
@ -220,10 +220,10 @@
"type": "Link" "type": "Link"
} }
], ],
"modified": "2021-02-18 17:31:53.586508", "modified": "2021-05-25 17:31:53.586508",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Loan Management", "module": "Loan Management",
"name": "Loan Management", "name": "Loans",
"owner": "Administrator", "owner": "Administrator",
"pin_to_bottom": 0, "pin_to_bottom": 0,
"pin_to_top": 0, "pin_to_top": 0,

View File

@ -2,15 +2,11 @@
// License: GNU General Public License v3. See license.txt // License: GNU General Public License v3. See license.txt
frappe.provide("erpnext.maintenance"); frappe.provide("erpnext.maintenance");
frappe.ui.form.on('Maintenance Schedule', { frappe.ui.form.on('Maintenance Schedule', {
setup: function (frm) { setup: function (frm) {
frm.set_query('contact_person', erpnext.queries.contact_query); frm.set_query('contact_person', erpnext.queries.contact_query);
frm.set_query('customer_address', erpnext.queries.address_query); frm.set_query('customer_address', erpnext.queries.address_query);
frm.set_query('customer', erpnext.queries.customer); frm.set_query('customer', erpnext.queries.customer);
frm.add_fetch('item_code', 'item_name', 'item_name');
frm.add_fetch('item_code', 'description', 'description');
}, },
onload: function (frm) { onload: function (frm) {
if (!frm.doc.status) { if (!frm.doc.status) {
@ -47,7 +43,7 @@ frappe.ui.form.on('Maintenance Schedule', {
// TODO commonify this code // TODO commonify this code
erpnext.maintenance.MaintenanceSchedule = frappe.ui.form.Controller.extend({ erpnext.maintenance.MaintenanceSchedule = frappe.ui.form.Controller.extend({
refresh: function () { refresh: function () {
frappe.dynamic_link = {doc: this.frm.doc, fieldname: 'customer', doctype: 'Customer'} frappe.dynamic_link = { doc: this.frm.doc, fieldname: 'customer', doctype: 'Customer' };
var me = this; var me = this;
@ -68,14 +64,80 @@ erpnext.maintenance.MaintenanceSchedule = frappe.ui.form.Controller.extend({
}); });
}, __("Get Items From")); }, __("Get Items From"));
} else if (this.frm.doc.docstatus === 1) { } else if (this.frm.doc.docstatus === 1) {
this.frm.add_custom_button(__('Create Maintenance Visit'), function() { let schedules = me.frm.doc.schedules;
frappe.model.open_mapped_doc({ let flag = schedules.some(schedule => schedule.completion_status === "Pending");
if (flag) {
this.frm.add_custom_button(__('Maintenance Visit'), function () {
let options = "";
me.frm.call('get_pending_data', {data_type: "items"}).then(r => {
options = r.message;
let schedule_id = "";
let d = new frappe.ui.Dialog({
title: __("Enter Visit Details"),
fields: [{
fieldtype: "Select",
fieldname: "item_name",
label: __("Item Name"),
options: options,
reqd: 1,
onchange: function () {
let field = d.get_field("scheduled_date");
me.frm.call('get_pending_data',
{
item_name: this.value,
data_type: "date"
}).then(r => {
field.df.options = r.message;
field.refresh();
});
}
},
{
label: __('Scheduled Date'),
fieldname: 'scheduled_date',
fieldtype: 'Select',
options: "",
reqd: 1,
onchange: function () {
let field = d.get_field('item_name');
me.frm.call(
'get_pending_data',
{
item_name: field.value,
s_date: this.value,
data_type: "id"
}).then(r => {
schedule_id = r.message;
});
}
},
],
primary_action_label: 'Create Visit',
primary_action(values) {
frappe.call({
method: "erpnext.maintenance.doctype.maintenance_schedule.maintenance_schedule.make_maintenance_visit", method: "erpnext.maintenance.doctype.maintenance_schedule.maintenance_schedule.make_maintenance_visit",
args: {
item_name: values.item_name,
s_id: schedule_id,
source_name: me.frm.doc.name, source_name: me.frm.doc.name,
frm: me.frm },
callback: function (r) {
if (!r.exc) {
frappe.model.sync(r.message);
frappe.set_route("Form", r.message.doctype, r.message.name);
}
}
});
d.hide();
}
});
d.show();
}); });
}, __('Create')); }, __('Create'));
} }
}
}, },
start_date: function (doc, cdt, cdn) { start_date: function (doc, cdt, cdn) {
@ -88,29 +150,18 @@ erpnext.maintenance.MaintenanceSchedule = frappe.ui.form.Controller.extend({
periodicity: function (doc, cdt, cdn) { periodicity: function (doc, cdt, cdn) {
this.set_no_of_visits(doc, cdt, cdn); this.set_no_of_visits(doc, cdt, cdn);
},
no_of_visits: function (doc, cdt, cdn) {
this.set_no_of_visits(doc, cdt, cdn);
}, },
set_no_of_visits: function (doc, cdt, cdn) { set_no_of_visits: function (doc, cdt, cdn) {
var item = frappe.get_doc(cdt, cdn); var item = frappe.get_doc(cdt, cdn);
let me = this;
if (item.start_date && item.periodicity) {
me.frm.call('validate_end_date_visits');
if (item.start_date && item.end_date && item.periodicity) {
if(item.start_date > item.end_date) {
frappe.msgprint(__("Row {0}:Start Date must be before End Date", [item.idx]));
return;
}
var date_diff = frappe.datetime.get_diff(item.end_date, item.start_date) + 1;
var days_in_period = {
"Weekly": 7,
"Monthly": 30,
"Quarterly": 91,
"Half Yearly": 182,
"Yearly": 365
}
var no_of_visits = cint(date_diff / days_in_period[item.periodicity]);
frappe.model.set_value(item.doctype, item.name, "no_of_visits", no_of_visits);
} }
}, },
}); });

View File

@ -1,819 +1,242 @@
{ {
"allow_copy": 0, "actions": [],
"allow_guest_to_view": 0,
"allow_import": 0,
"allow_rename": 0,
"autoname": "naming_series:", "autoname": "naming_series:",
"beta": 0,
"creation": "2013-01-10 16:34:30", "creation": "2013-01-10 16:34:30",
"custom": 0,
"docstatus": 0,
"doctype": "DocType", "doctype": "DocType",
"document_type": "Document", "document_type": "Document",
"editable_grid": 0, "engine": "InnoDB",
"field_order": [
"customer_details",
"naming_series",
"customer",
"column_break0",
"status",
"transaction_date",
"items_section",
"items",
"schedule",
"generate_schedule",
"schedules",
"contact_info",
"customer_name",
"contact_person",
"contact_mobile",
"contact_email",
"contact_display",
"column_break_17",
"customer_address",
"address_display",
"territory",
"customer_group",
"company",
"amended_from"
],
"fields": [ "fields": [
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "customer_details", "fieldname": "customer_details",
"fieldtype": "Section Break", "fieldtype": "Section 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,
"label": "",
"length": 0,
"no_copy": 0,
"oldfieldtype": "Section Break", "oldfieldtype": "Section Break",
"options": "fa fa-user", "options": "fa fa-user"
"permlevel": 0,
"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,
"translatable": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"default": "",
"fieldname": "naming_series", "fieldname": "naming_series",
"fieldtype": "Select", "fieldtype": "Select",
"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,
"label": "Series", "label": "Series",
"length": 0,
"no_copy": 1, "no_copy": 1,
"options": "MAT-MSH-.YYYY.-", "options": "MAT-MSH-.YYYY.-",
"permlevel": 0,
"precision": "",
"print_hide": 1, "print_hide": 1,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1, "reqd": 1,
"search_index": 0, "set_only_once": 1
"set_only_once": 1,
"translatable": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "customer", "fieldname": "customer",
"fieldtype": "Link", "fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 1, "in_standard_filter": 1,
"label": "Customer", "label": "Customer",
"length": 0,
"no_copy": 0,
"oldfieldname": "customer", "oldfieldname": "customer",
"oldfieldtype": "Link", "oldfieldtype": "Link",
"options": "Customer", "options": "Customer",
"permlevel": 0,
"print_hide": 1, "print_hide": 1,
"print_hide_if_no_value": 0, "search_index": 1
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 1,
"set_only_once": 0,
"translatable": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "column_break0", "fieldname": "column_break0",
"fieldtype": "Column Break", "fieldtype": "Column Break",
"hidden": 0, "oldfieldtype": "Column Break"
"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,
"oldfieldtype": "Column Break",
"permlevel": 0,
"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,
"translatable": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"default": "Draft", "default": "Draft",
"fieldname": "status", "fieldname": "status",
"fieldtype": "Select", "fieldtype": "Select",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 1, "in_standard_filter": 1,
"label": "Status", "label": "Status",
"length": 0,
"no_copy": 1, "no_copy": 1,
"oldfieldname": "status", "oldfieldname": "status",
"oldfieldtype": "Select", "oldfieldtype": "Select",
"options": "\nDraft\nSubmitted\nCancelled", "options": "\nDraft\nSubmitted\nCancelled",
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 1, "read_only": 1,
"remember_last_selected_value": 0, "reqd": 1
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "transaction_date", "fieldname": "transaction_date",
"fieldtype": "Date", "fieldtype": "Date",
"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,
"label": "Transaction Date", "label": "Transaction Date",
"length": 0,
"no_copy": 0,
"oldfieldname": "transaction_date", "oldfieldname": "transaction_date",
"oldfieldtype": "Date", "oldfieldtype": "Date",
"permlevel": 0, "reqd": 1
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "items_section", "fieldname": "items_section",
"fieldtype": "Section Break", "fieldtype": "Section 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,
"label": "",
"length": 0,
"no_copy": 0,
"oldfieldtype": "Section Break", "oldfieldtype": "Section Break",
"options": "fa fa-shopping-cart", "options": "fa fa-shopping-cart"
"permlevel": 0,
"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,
"translatable": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "items", "fieldname": "items",
"fieldtype": "Table", "fieldtype": "Table",
"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,
"label": "Items", "label": "Items",
"length": 0,
"no_copy": 0,
"oldfieldname": "item_maintenance_detail", "oldfieldname": "item_maintenance_detail",
"oldfieldtype": "Table", "oldfieldtype": "Table",
"options": "Maintenance Schedule Item", "options": "Maintenance Schedule Item",
"permlevel": 0, "reqd": 1
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "schedule", "fieldname": "schedule",
"fieldtype": "Section Break", "fieldtype": "Section 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,
"label": "Schedule", "label": "Schedule",
"length": 0,
"no_copy": 0,
"oldfieldtype": "Section Break", "oldfieldtype": "Section Break",
"options": "fa fa-time", "options": "fa fa-time"
"permlevel": 0,
"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,
"translatable": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "generate_schedule", "fieldname": "generate_schedule",
"fieldtype": "Button", "fieldtype": "Button",
"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,
"label": "Generate Schedule", "label": "Generate Schedule",
"length": 0, "oldfieldtype": "Button"
"no_copy": 0,
"oldfieldtype": "Button",
"permlevel": 0,
"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,
"translatable": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "schedules", "fieldname": "schedules",
"fieldtype": "Table", "fieldtype": "Table",
"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,
"label": "Schedules", "label": "Schedules",
"length": 0,
"no_copy": 0,
"oldfieldname": "schedules", "oldfieldname": "schedules",
"oldfieldtype": "Table", "oldfieldtype": "Table",
"options": "Maintenance Schedule Detail", "options": "Maintenance Schedule Detail"
"permlevel": 0,
"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,
"translatable": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "contact_info", "fieldname": "contact_info",
"fieldtype": "Section Break", "fieldtype": "Section Break",
"hidden": 0, "label": "Contact Info"
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Contact Info",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"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,
"translatable": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 1, "bold": 1,
"collapsible": 0,
"columns": 0,
"depends_on": "customer", "depends_on": "customer",
"fieldname": "customer_name", "fieldname": "customer_name",
"fieldtype": "Data", "fieldtype": "Data",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 1, "in_global_search": 1,
"in_list_view": 1, "in_list_view": 1,
"in_standard_filter": 0,
"label": "Customer Name", "label": "Customer Name",
"length": 0,
"no_copy": 0,
"oldfieldname": "customer_name", "oldfieldname": "customer_name",
"oldfieldtype": "Data", "oldfieldtype": "Data",
"permlevel": 0, "read_only": 1
"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,
"translatable": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"depends_on": "customer", "depends_on": "customer",
"fieldname": "contact_person", "fieldname": "contact_person",
"fieldtype": "Link", "fieldtype": "Link",
"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,
"label": "Contact Person", "label": "Contact Person",
"length": 0,
"no_copy": 0,
"options": "Contact", "options": "Contact",
"permlevel": 0, "print_hide": 1
"print_hide": 1,
"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,
"translatable": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"depends_on": "customer", "depends_on": "customer",
"fieldname": "contact_mobile", "fieldname": "contact_mobile",
"fieldtype": "Data", "fieldtype": "Data",
"hidden": 1, "hidden": 1,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Mobile No", "label": "Mobile No",
"length": 0, "read_only": 1
"no_copy": 0,
"permlevel": 0,
"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,
"translatable": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"depends_on": "customer", "depends_on": "customer",
"fieldname": "contact_email", "fieldname": "contact_email",
"fieldtype": "Data", "fieldtype": "Data",
"hidden": 1, "hidden": 1,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Contact Email", "label": "Contact Email",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"print_hide": 1, "print_hide": 1,
"print_hide_if_no_value": 0, "read_only": 1
"read_only": 1,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "contact_display", "fieldname": "contact_display",
"fieldtype": "Small Text", "fieldtype": "Small Text",
"hidden": 1, "hidden": 1,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 1, "in_global_search": 1,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Contact", "label": "Contact",
"length": 0, "read_only": 1
"no_copy": 0,
"permlevel": 0,
"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,
"translatable": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "column_break_17", "fieldname": "column_break_17",
"fieldtype": "Column Break", "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,
"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,
"translatable": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"depends_on": "customer", "depends_on": "customer",
"fieldname": "customer_address", "fieldname": "customer_address",
"fieldtype": "Link", "fieldtype": "Link",
"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,
"label": "Customer Address", "label": "Customer Address",
"length": 0,
"no_copy": 0,
"options": "Address", "options": "Address",
"permlevel": 0, "print_hide": 1
"print_hide": 1,
"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,
"translatable": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "address_display", "fieldname": "address_display",
"fieldtype": "Small Text", "fieldtype": "Small Text",
"hidden": 1, "hidden": 1,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Address", "label": "Address",
"length": 0, "read_only": 1
"no_copy": 0,
"permlevel": 0,
"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,
"translatable": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"depends_on": "customer", "depends_on": "customer",
"description": "",
"fieldname": "territory", "fieldname": "territory",
"fieldtype": "Link", "fieldtype": "Link",
"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,
"label": "Territory", "label": "Territory",
"length": 0,
"no_copy": 0,
"oldfieldname": "territory", "oldfieldname": "territory",
"oldfieldtype": "Link", "oldfieldtype": "Link",
"options": "Territory", "options": "Territory"
"permlevel": 0,
"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,
"translatable": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"depends_on": "customer", "depends_on": "customer",
"description": "",
"fieldname": "customer_group", "fieldname": "customer_group",
"fieldtype": "Link", "fieldtype": "Link",
"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,
"label": "Customer Group", "label": "Customer Group",
"length": 0, "options": "Customer Group"
"no_copy": 0,
"options": "Customer Group",
"permlevel": 0,
"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,
"translatable": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "company", "fieldname": "company",
"fieldtype": "Link", "fieldtype": "Link",
"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,
"label": "Company", "label": "Company",
"length": 0,
"no_copy": 0,
"oldfieldname": "company", "oldfieldname": "company",
"oldfieldtype": "Link", "oldfieldtype": "Link",
"options": "Company", "options": "Company",
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 1, "remember_last_selected_value": 1,
"report_hide": 0, "reqd": 1
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "amended_from", "fieldname": "amended_from",
"fieldtype": "Link", "fieldtype": "Link",
"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,
"label": "Amended From", "label": "Amended From",
"length": 0,
"no_copy": 1, "no_copy": 1,
"options": "Maintenance Schedule", "options": "Maintenance Schedule",
"permlevel": 0,
"print_hide": 1, "print_hide": 1,
"print_hide_if_no_value": 0, "read_only": 1
"read_only": 1,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
} }
], ],
"has_web_view": 0,
"hide_heading": 0,
"hide_toolbar": 0,
"icon": "fa fa-calendar", "icon": "fa fa-calendar",
"idx": 1, "idx": 1,
"image_view": 0,
"in_create": 0,
"is_submittable": 1, "is_submittable": 1,
"issingle": 0, "links": [
"istable": 0, {
"max_attachments": 0, "group": "Visits",
"modified": "2020-09-18 17:26:09.703215", "link_doctype": "Maintenance Visit",
"link_fieldname": "maintenance_schedule"
}
],
"modified": "2021-05-27 16:05:10.746465",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Maintenance", "module": "Maintenance",
"name": "Maintenance Schedule", "name": "Maintenance Schedule",
@ -825,28 +248,17 @@
"create": 1, "create": 1,
"delete": 1, "delete": 1,
"email": 1, "email": 1,
"export": 0,
"if_owner": 0,
"import": 0,
"permlevel": 0,
"print": 1, "print": 1,
"read": 1, "read": 1,
"report": 1, "report": 1,
"role": "Maintenance Manager", "role": "Maintenance Manager",
"set_user_permissions": 0,
"share": 1, "share": 1,
"submit": 1, "submit": 1,
"write": 1 "write": 1
} }
], ],
"quick_entry": 0,
"read_only": 0,
"read_only_onload": 0,
"search_fields": "status,customer,customer_name", "search_fields": "status,customer,customer_name",
"show_name_in_global_search": 0, "sort_field": "modified",
"sort_order": "DESC", "sort_order": "DESC",
"timeline_field": "customer", "timeline_field": "customer"
"track_changes": 0,
"track_seen": 0,
"track_views": 0
} }

View File

@ -4,12 +4,13 @@
from __future__ import unicode_literals from __future__ import unicode_literals
import frappe import frappe
from frappe.utils import add_days, getdate, cint, cstr from frappe.utils import add_days, getdate, cint, cstr, date_diff, formatdate
from frappe import throw, _ from frappe import throw, _
from erpnext.utilities.transaction_base import TransactionBase, delete_events from erpnext.utilities.transaction_base import TransactionBase, delete_events
from erpnext.stock.utils import get_valid_serial_nos from erpnext.stock.utils import get_valid_serial_nos
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.stock.doctype.serial_no.serial_no import get_serial_nos
class MaintenanceSchedule(TransactionBase): class MaintenanceSchedule(TransactionBase):
@frappe.whitelist() @frappe.whitelist()
@ -32,8 +33,40 @@ class MaintenanceSchedule(TransactionBase):
child.idx = count child.idx = count
count = count + 1 count = count + 1
child.sales_person = d.sales_person child.sales_person = d.sales_person
child.completion_status = "Pending"
child.item_reference = d.name
@frappe.whitelist()
def validate_end_date_visits(self):
days_in_period = {
"Weekly": 7,
"Monthly": 30,
"Quarterly": 91,
"Half Yearly": 182,
"Yearly": 365
}
for item in self.items:
if item.periodicity and item.start_date:
if not item.end_date:
if item.no_of_visits:
item.end_date = add_days(item.start_date, item.no_of_visits * days_in_period[item.periodicity])
else:
item.end_date = add_days(item.start_date, days_in_period[item.periodicity])
diff = date_diff(item.end_date, item.start_date) + 1
no_of_visits = cint(diff / days_in_period[item.periodicity])
if not item.no_of_visits or item.no_of_visits == 0:
item.end_date = add_days(item.start_date, days_in_period[item.periodicity])
diff = date_diff(item.end_date, item.start_date) + 1
item.no_of_visits = cint(diff / days_in_period[item.periodicity])
elif item.no_of_visits > no_of_visits:
item.end_date = add_days(item.start_date, item.no_of_visits * days_in_period[item.periodicity])
elif item.no_of_visits < no_of_visits:
item.end_date = add_days(item.start_date, item.no_of_visits * days_in_period[item.periodicity])
self.save()
def on_submit(self): def on_submit(self):
if not self.get('schedules'): if not self.get('schedules'):
@ -58,9 +91,10 @@ class MaintenanceSchedule(TransactionBase):
if no_email_sp: if no_email_sp:
frappe.msgprint( frappe.msgprint(
frappe._("Setting Events to {0}, since the Employee attached to the below Sales Persons does not have a User ID{1}").format( _("Setting Events to {0}, since the Employee attached to the below Sales Persons does not have a User ID{1}").format(
self.owner, "<br>" + "<br>".join(no_email_sp) self.owner, "<br>" + "<br>".join(no_email_sp)
)) )
)
scheduled_date = frappe.db.sql("""select scheduled_date from scheduled_date = frappe.db.sql("""select scheduled_date from
`tabMaintenance Schedule Detail` where sales_person=%s and item_code=%s and `tabMaintenance Schedule Detail` where sales_person=%s and item_code=%s and
@ -135,8 +169,7 @@ class MaintenanceSchedule(TransactionBase):
} }
if date_diff < days_in_period[d.periodicity]: if date_diff < days_in_period[d.periodicity]:
throw(_("Row {0}: To set {1} periodicity, difference between from and to date \ throw(_("Row {0}: To set {1} periodicity, difference between from and to date must be greater than or equal to {2}")
must be greater than or equal to {2}")
.format(d.idx, d.periodicity, days_in_period[d.periodicity])) .format(d.idx, d.periodicity, days_in_period[d.periodicity]))
def validate_maintenance_detail(self): def validate_maintenance_detail(self):
@ -166,9 +199,11 @@ class MaintenanceSchedule(TransactionBase):
throw(_("Maintenance Schedule {0} exists against {1}").format(chk[0][0], d.sales_order)) throw(_("Maintenance Schedule {0} exists against {1}").format(chk[0][0], d.sales_order))
def validate(self): def validate(self):
self.validate_end_date_visits()
self.validate_maintenance_detail() self.validate_maintenance_detail()
self.validate_dates_with_periodicity() self.validate_dates_with_periodicity()
self.validate_sales_order() self.validate_sales_order()
self.generate_schedule()
def on_update(self): def on_update(self):
frappe.db.set(self, 'status', 'Draft') frappe.db.set(self, 'status', 'Draft')
@ -246,11 +281,48 @@ class MaintenanceSchedule(TransactionBase):
delete_events(self.doctype, self.name) delete_events(self.doctype, self.name)
@frappe.whitelist() @frappe.whitelist()
def make_maintenance_visit(source_name, target_doc=None): def get_pending_data(self, data_type, s_date=None, item_name=None):
if data_type == "date":
dates = ""
for schedule in self.schedules:
if schedule.item_name == item_name and schedule.completion_status == "Pending":
dates = dates + "\n" + formatdate(schedule.scheduled_date, "dd-MM-yyyy")
return dates
elif data_type == "items":
items = ""
for item in self.items:
for schedule in self.schedules:
if item.item_name == schedule.item_name and schedule.completion_status == "Pending":
items = items + "\n" + item.item_name
break
return items
elif data_type == "id":
for schedule in self.schedules:
if schedule.item_name == item_name and s_date == formatdate(schedule.scheduled_date, "dd-mm-yyyy"):
return schedule.name
@frappe.whitelist()
def update_serial_nos(s_id):
serial_nos = frappe.db.get_value('Maintenance Schedule Detail', s_id, 'serial_no')
if serial_nos:
serial_nos = get_serial_nos(serial_nos)
return serial_nos
else:
return False
@frappe.whitelist()
def make_maintenance_visit(source_name, target_doc=None, item_name=None, s_id=None):
from frappe.model.mapper import get_mapped_doc from frappe.model.mapper import get_mapped_doc
def update_status(source, target, parent): def update_status_and_detail(source, target, parent):
target.maintenance_type = "Scheduled" target.maintenance_type = "Scheduled"
target.maintenance_schedule = source.name
target.maintenance_schedule_detail = s_id
def update_sales(source, target, parent):
sales_person = frappe.db.get_value('Maintenance Schedule Detail', s_id, 'sales_person')
target.service_person = sales_person
target.serial_no = ''
doclist = get_mapped_doc("Maintenance Schedule", source_name, { doclist = get_mapped_doc("Maintenance Schedule", source_name, {
"Maintenance Schedule": { "Maintenance Schedule": {
@ -261,15 +333,12 @@ def make_maintenance_visit(source_name, target_doc=None):
"validation": { "validation": {
"docstatus": ["=", 1] "docstatus": ["=", 1]
}, },
"postprocess": update_status "postprocess": update_status_and_detail
}, },
"Maintenance Schedule Item": { "Maintenance Schedule Item": {
"doctype": "Maintenance Visit Purpose", "doctype": "Maintenance Visit Purpose",
"field_map": { "condition": lambda doc: doc.item_name == item_name,
"parent": "prevdoc_docname", "postprocess": update_sales
"parenttype": "prevdoc_doctype",
"sales_person": "service_person"
}
} }
}, target_doc) }, target_doc)

View File

@ -2,7 +2,8 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors # Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt # See license.txt
from __future__ import unicode_literals from __future__ import unicode_literals
from frappe.utils.data import get_datetime, add_days from frappe.utils.data import add_days, today, formatdate
from erpnext.maintenance.doctype.maintenance_schedule.maintenance_schedule import make_maintenance_visit
import frappe import frappe
import unittest import unittest
@ -22,6 +23,56 @@ class TestMaintenanceSchedule(unittest.TestCase):
events_after_cancel = get_events(ms) events_after_cancel = get_events(ms)
self.assertTrue(len(events_after_cancel) == 0) self.assertTrue(len(events_after_cancel) == 0)
def test_make_schedule(self):
ms = make_maintenance_schedule()
ms.save()
i = ms.items[0]
expected_dates = []
expected_end_date = add_days(i.start_date, i.no_of_visits * 7)
self.assertEqual(i.end_date, expected_end_date)
i.no_of_visits = 2
ms.save()
expected_end_date = add_days(i.start_date, i.no_of_visits * 7)
self.assertEqual(i.end_date, expected_end_date)
items = ms.get_pending_data(data_type = "items")
items = items.split('\n')
items.pop(0)
expected_items = ['_Test Item']
self.assertTrue(items, expected_items)
# "dates" contains all generated schedule dates
dates = ms.get_pending_data(data_type = "date", item_name = i.item_name)
dates = dates.split('\n')
dates.pop(0)
expected_dates.append(formatdate(add_days(i.start_date, 7), "dd-MM-yyyy"))
expected_dates.append(formatdate(add_days(i.start_date, 14), "dd-MM-yyyy"))
# test for generated schedule dates
self.assertEqual(dates, expected_dates)
ms.submit()
s_id = ms.get_pending_data(data_type = "id", item_name = i.item_name, s_date = expected_dates[1])
test = make_maintenance_visit(source_name = ms.name, item_name = "_Test Item", s_id = s_id)
visit = frappe.new_doc('Maintenance Visit')
visit = test
visit.maintenance_schedule = ms.name
visit.maintenance_schedule_detail = s_id
visit.completion_status = "Partially Completed"
visit.set('purposes', [{
'item_code': i.item_code,
'description': "test",
'work_done': "test",
'service_person': "Sales Team",
}])
visit.save()
visit.submit()
ms = frappe.get_doc('Maintenance Schedule', ms.name)
#checks if visit status is back updated in schedule
self.assertTrue(ms.schedules[1].completion_status, "Partially Completed")
def get_events(ms): def get_events(ms):
return frappe.get_all("Event Participants", filters={ return frappe.get_all("Event Participants", filters={
"reference_doctype": ms.doctype, "reference_doctype": ms.doctype,
@ -33,12 +84,11 @@ def make_maintenance_schedule():
ms = frappe.new_doc("Maintenance Schedule") ms = frappe.new_doc("Maintenance Schedule")
ms.company = "_Test Company" ms.company = "_Test Company"
ms.customer = "_Test Customer" ms.customer = "_Test Customer"
ms.transaction_date = get_datetime() ms.transaction_date = today()
ms.append("items", { ms.append("items", {
"item_code": "_Test Item", "item_code": "_Test Item",
"start_date": get_datetime(), "start_date": today(),
"end_date": add_days(get_datetime(), 32),
"periodicity": "Weekly", "periodicity": "Weekly",
"no_of_visits": 4, "no_of_visits": 4,
"sales_person": "Sales Team", "sales_person": "Sales Team",

View File

@ -1,222 +1,137 @@
{ {
"allow_copy": 0, "actions": [],
"allow_import": 0,
"allow_rename": 0,
"autoname": "hash", "autoname": "hash",
"beta": 0,
"creation": "2013-02-22 01:28:05", "creation": "2013-02-22 01:28:05",
"custom": 0,
"docstatus": 0,
"doctype": "DocType", "doctype": "DocType",
"document_type": "Document", "document_type": "Document",
"editable_grid": 1, "editable_grid": 1,
"engine": "InnoDB", "engine": "InnoDB",
"field_order": [
"item_code",
"item_name",
"column_break_3",
"scheduled_date",
"actual_date",
"section_break_6",
"sales_person",
"column_break_8",
"completion_status",
"section_break_10",
"serial_no",
"item_reference"
],
"fields": [ "fields": [
{ {
"allow_on_submit": 0, "columns": 2,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "item_code", "fieldname": "item_code",
"fieldtype": "Link", "fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1, "in_list_view": 1,
"in_standard_filter": 0,
"label": "Item Code", "label": "Item Code",
"length": 0,
"no_copy": 0,
"oldfieldname": "item_code", "oldfieldname": "item_code",
"oldfieldtype": "Link", "oldfieldtype": "Link",
"options": "Item", "options": "Item",
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 1, "read_only": 1,
"remember_last_selected_value": 0, "search_index": 1
"report_hide": 0,
"reqd": 0,
"search_index": 1,
"set_only_once": 0,
"unique": 0
}, },
{ {
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "item_name", "fieldname": "item_name",
"fieldtype": "Data", "fieldtype": "Data",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 1, "in_global_search": 1,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Item Name", "label": "Item Name",
"length": 0,
"no_copy": 0,
"oldfieldname": "item_name", "oldfieldname": "item_name",
"oldfieldtype": "Data", "oldfieldtype": "Data",
"permlevel": 0, "read_only": 1
"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_on_submit": 0, "columns": 2,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "scheduled_date", "fieldname": "scheduled_date",
"fieldtype": "Date", "fieldtype": "Date",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1, "in_list_view": 1,
"in_standard_filter": 0,
"label": "Scheduled Date", "label": "Scheduled Date",
"length": 0,
"no_copy": 0,
"oldfieldname": "scheduled_date", "oldfieldname": "scheduled_date",
"oldfieldtype": "Date", "oldfieldtype": "Date",
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1, "reqd": 1,
"search_index": 1, "search_index": 1
"set_only_once": 0,
"unique": 0
}, },
{ {
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "actual_date", "fieldname": "actual_date",
"fieldtype": "Date", "fieldtype": "Date",
"hidden": 1, "in_list_view": 1,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Actual Date", "label": "Actual Date",
"length": 0,
"no_copy": 1, "no_copy": 1,
"oldfieldname": "actual_date", "oldfieldname": "actual_date",
"oldfieldtype": "Date", "oldfieldtype": "Date",
"permlevel": 0,
"print_hide": 1, "print_hide": 1,
"print_hide_if_no_value": 0, "read_only": 1,
"read_only": 0, "report_hide": 1
"remember_last_selected_value": 0,
"report_hide": 1,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
}, },
{ {
"allow_on_submit": 0, "allow_on_submit": 1,
"bold": 0, "columns": 2,
"collapsible": 0,
"columns": 0,
"fieldname": "sales_person", "fieldname": "sales_person",
"fieldtype": "Link", "fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1, "in_list_view": 1,
"in_standard_filter": 0,
"label": "Sales Person", "label": "Sales Person",
"length": 0,
"no_copy": 0,
"oldfieldname": "incharge_name", "oldfieldname": "incharge_name",
"oldfieldtype": "Link", "oldfieldtype": "Link",
"options": "Sales Person", "options": "Sales Person",
"permlevel": 0, "read_only_depends_on": "eval:doc.completion_status != \"Pending\""
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"unique": 0
}, },
{ {
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "serial_no", "fieldname": "serial_no",
"fieldtype": "Small Text", "fieldtype": "Small Text",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1, "in_list_view": 1,
"in_standard_filter": 0,
"label": "Serial No", "label": "Serial No",
"length": 0,
"no_copy": 0,
"oldfieldname": "serial_no", "oldfieldname": "serial_no",
"oldfieldtype": "Small Text", "oldfieldtype": "Small Text",
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"print_width": "160px", "print_width": "160px",
"read_only": 1, "read_only": 1,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0,
"width": "160px" "width": "160px"
},
{
"columns": 2,
"fieldname": "completion_status",
"fieldtype": "Select",
"in_list_view": 1,
"label": "Completion Status",
"options": "Pending\nPartially Completed\nFully Completed",
"read_only": 1
},
{
"fieldname": "column_break_3",
"fieldtype": "Column Break"
},
{
"fieldname": "section_break_6",
"fieldtype": "Section Break"
},
{
"fieldname": "column_break_8",
"fieldtype": "Column Break"
},
{
"fieldname": "section_break_10",
"fieldtype": "Section Break"
},
{
"fieldname": "item_reference",
"fieldtype": "Link",
"hidden": 1,
"label": "Item Reference",
"options": "Maintenance Schedule Item",
"read_only": 1
} }
], ],
"hide_heading": 0,
"hide_toolbar": 0,
"idx": 1, "idx": 1,
"image_view": 0,
"in_create": 0,
"is_submittable": 0,
"issingle": 0,
"istable": 1, "istable": 1,
"max_attachments": 0, "links": [],
"modified": "2017-02-17 17:05:44.644663", "modified": "2021-05-27 16:07:25.905015",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Maintenance", "module": "Maintenance",
"name": "Maintenance Schedule Detail", "name": "Maintenance Schedule Detail",
"owner": "Administrator", "owner": "Administrator",
"permissions": [], "permissions": [],
"quick_entry": 0, "sort_field": "modified",
"read_only": 0, "sort_order": "DESC",
"read_only_onload": 0, "track_changes": 1
"show_name_in_global_search": 0,
"track_changes": 1,
"track_seen": 0
} }

View File

@ -1,431 +1,160 @@
{ {
"allow_copy": 0, "actions": [],
"allow_guest_to_view": 0,
"allow_import": 0,
"allow_rename": 0,
"autoname": "hash", "autoname": "hash",
"beta": 0,
"creation": "2013-02-22 01:28:05", "creation": "2013-02-22 01:28:05",
"custom": 0,
"docstatus": 0,
"doctype": "DocType", "doctype": "DocType",
"document_type": "Document", "document_type": "Document",
"editable_grid": 1, "editable_grid": 1,
"engine": "InnoDB", "engine": "InnoDB",
"field_order": [
"item_code",
"item_name",
"description",
"column_break_4",
"start_date",
"end_date",
"periodicity",
"schedule_details",
"no_of_visits",
"column_break_10",
"sales_person",
"reference",
"serial_no",
"sales_order"
],
"fields": [ "fields": [
{ {
"allow_bulk_edit": 0, "columns": 2,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "item_code", "fieldname": "item_code",
"fieldtype": "Link", "fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1, "in_list_view": 1,
"in_standard_filter": 0,
"label": "Item Code", "label": "Item Code",
"length": 0,
"no_copy": 0,
"oldfieldname": "item_code", "oldfieldname": "item_code",
"oldfieldtype": "Link", "oldfieldtype": "Link",
"options": "Item", "options": "Item",
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1, "reqd": 1,
"search_index": 1, "search_index": 1
"set_only_once": 0,
"translatable": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0, "columns": 1,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fetch_from": "item_code.item_name", "fetch_from": "item_code.item_name",
"fieldname": "item_name", "fieldname": "item_name",
"fieldtype": "Data", "fieldtype": "Data",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 1, "in_global_search": 1,
"in_list_view": 1, "in_list_view": 1,
"in_standard_filter": 0,
"label": "Item Name", "label": "Item Name",
"length": 0,
"no_copy": 0,
"oldfieldname": "item_name", "oldfieldname": "item_name",
"oldfieldtype": "Data", "oldfieldtype": "Data",
"options": "", "read_only": 1
"permlevel": 0,
"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,
"translatable": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fetch_from": "item_code.description", "fetch_from": "item_code.description",
"fieldname": "description", "fieldname": "description",
"fieldtype": "Text Editor", "fieldtype": "Text Editor",
"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,
"label": "Description", "label": "Description",
"length": 0,
"no_copy": 0,
"oldfieldname": "description", "oldfieldname": "description",
"oldfieldtype": "Data", "oldfieldtype": "Data",
"options": "",
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"print_width": "300px", "print_width": "300px",
"read_only": 1, "read_only": 1,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0,
"width": "300px" "width": "300px"
}, },
{ {
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "schedule_details", "fieldname": "schedule_details",
"fieldtype": "Section Break", "fieldtype": "Section 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,
"label": "",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"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,
"translatable": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0, "columns": 2,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "start_date", "fieldname": "start_date",
"fieldtype": "Date", "fieldtype": "Date",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1, "in_list_view": 1,
"in_standard_filter": 0,
"label": "Start Date", "label": "Start Date",
"length": 0,
"no_copy": 0,
"oldfieldname": "start_date", "oldfieldname": "start_date",
"oldfieldtype": "Date", "oldfieldtype": "Date",
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1, "reqd": 1,
"search_index": 1, "search_index": 1
"set_only_once": 0,
"translatable": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0, "columns": 2,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "end_date", "fieldname": "end_date",
"fieldtype": "Date", "fieldtype": "Date",
"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": "End Date", "label": "End Date",
"length": 0,
"no_copy": 0,
"oldfieldname": "end_date", "oldfieldname": "end_date",
"oldfieldtype": "Date", "oldfieldtype": "Date",
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1, "reqd": 1,
"search_index": 1, "search_index": 1
"set_only_once": 0,
"translatable": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0, "columns": 1,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "periodicity", "fieldname": "periodicity",
"fieldtype": "Select", "fieldtype": "Select",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1, "in_list_view": 1,
"in_standard_filter": 0,
"label": "Periodicity", "label": "Periodicity",
"length": 0,
"no_copy": 0,
"oldfieldname": "periodicity", "oldfieldname": "periodicity",
"oldfieldtype": "Select", "oldfieldtype": "Select",
"options": "\nWeekly\nMonthly\nQuarterly\nHalf Yearly\nYearly\nRandom", "options": "\nWeekly\nMonthly\nQuarterly\nHalf Yearly\nYearly\nRandom"
"permlevel": 0,
"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,
"translatable": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0, "columns": 1,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "no_of_visits", "fieldname": "no_of_visits",
"fieldtype": "Int", "fieldtype": "Int",
"hidden": 0, "in_list_view": 1,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "No of Visits", "label": "No of Visits",
"length": 0,
"no_copy": 0,
"oldfieldname": "no_of_visits", "oldfieldname": "no_of_visits",
"oldfieldtype": "Int", "oldfieldtype": "Int",
"permlevel": 0, "reqd": 1
"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,
"translatable": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "sales_person", "fieldname": "sales_person",
"fieldtype": "Link", "fieldtype": "Link",
"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,
"label": "Sales Person", "label": "Sales Person",
"length": 0,
"no_copy": 0,
"oldfieldname": "incharge_name", "oldfieldname": "incharge_name",
"oldfieldtype": "Link", "oldfieldtype": "Link",
"options": "Sales Person", "options": "Sales Person"
"permlevel": 0,
"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,
"translatable": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "reference", "fieldname": "reference",
"fieldtype": "Section Break", "fieldtype": "Section Break",
"hidden": 0, "label": "Reference"
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Reference",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"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,
"translatable": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "serial_no", "fieldname": "serial_no",
"fieldtype": "Small Text", "fieldtype": "Small Text",
"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,
"label": "Serial No", "label": "Serial No",
"length": 0,
"no_copy": 0,
"oldfieldname": "serial_no", "oldfieldname": "serial_no",
"oldfieldtype": "Small Text", "oldfieldtype": "Small Text"
"permlevel": 0,
"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,
"translatable": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "sales_order", "fieldname": "sales_order",
"fieldtype": "Link", "fieldtype": "Link",
"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,
"label": "Sales Order", "label": "Sales Order",
"length": 0,
"no_copy": 1, "no_copy": 1,
"oldfieldname": "prevdoc_docname", "oldfieldname": "prevdoc_docname",
"oldfieldtype": "Data", "oldfieldtype": "Data",
"options": "Sales Order", "options": "Sales Order",
"permlevel": 0,
"print_hide": 1, "print_hide": 1,
"print_hide_if_no_value": 0,
"print_width": "150px", "print_width": "150px",
"read_only": 1, "read_only": 1,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 1, "search_index": 1,
"set_only_once": 0,
"translatable": 0,
"unique": 0,
"width": "150px" "width": "150px"
},
{
"fieldname": "column_break_4",
"fieldtype": "Column Break"
},
{
"fieldname": "column_break_10",
"fieldtype": "Column Break"
} }
], ],
"has_web_view": 0,
"hide_heading": 0,
"hide_toolbar": 0,
"idx": 1, "idx": 1,
"image_view": 0,
"in_create": 0,
"is_submittable": 0,
"issingle": 0,
"istable": 1, "istable": 1,
"max_attachments": 0, "links": [],
"modified": "2018-05-16 22:43:14.260729", "modified": "2021-04-15 16:09:47.311994",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Maintenance", "module": "Maintenance",
"name": "Maintenance Schedule Item", "name": "Maintenance Schedule Item",
"owner": "Administrator", "owner": "Administrator",
"permissions": [], "permissions": [],
"quick_entry": 0, "sort_field": "modified",
"read_only": 0, "sort_order": "DESC"
"read_only_onload": 0,
"show_name_in_global_search": 0,
"track_changes": 0,
"track_seen": 0
} }

View File

@ -2,17 +2,26 @@
// License: GNU General Public License v3. See license.txt // License: GNU General Public License v3. See license.txt
frappe.provide("erpnext.maintenance"); frappe.provide("erpnext.maintenance");
var serial_nos = [];
frappe.ui.form.on('Maintenance Visit', { frappe.ui.form.on('Maintenance Visit', {
refresh: function (frm) { refresh: function (frm) {
//filters for serial_no based on item_code //filters for serial_no based on item_code
frm.set_query('serial_no', 'purposes', function (frm, cdt, cdn) { frm.set_query('serial_no', 'purposes', function (frm, cdt, cdn) {
let item = locals[cdt][cdn]; let item = locals[cdt][cdn];
if (serial_nos) {
return {
filters: {
'item_code': item.item_code,
'name': ["in", serial_nos]
}
};
} else {
return { return {
filters: { filters: {
'item_code': item.item_code 'item_code': item.item_code
} }
}; };
}
}); });
}, },
setup: function (frm) { setup: function (frm) {
@ -20,7 +29,21 @@ frappe.ui.form.on('Maintenance Visit', {
frm.set_query('customer_address', erpnext.queries.address_query); frm.set_query('customer_address', erpnext.queries.address_query);
frm.set_query('customer', erpnext.queries.customer); frm.set_query('customer', erpnext.queries.customer);
}, },
onload: function(frm) { onload: function (frm, cdt, cdn) {
let item = locals[cdt][cdn];
if (frm.maintenance_type == 'Scheduled') {
let schedule_id = item.purposes[0].prevdoc_detail_docname;
frappe.call({
method: "erpnext.maintenance.doctype.maintenance_schedule.maintenance_schedule.update_serial_nos",
args: {
s_id: schedule_id
},
callback: function (r) {
serial_nos = r.message;
}
});
}
if (!frm.doc.status) { if (!frm.doc.status) {
frm.set_value({ status: 'Draft' }); frm.set_value({ status: 'Draft' });
} }
@ -43,7 +66,7 @@ frappe.ui.form.on('Maintenance Visit', {
// TODO commonify this code // TODO commonify this code
erpnext.maintenance.MaintenanceVisit = frappe.ui.form.Controller.extend({ erpnext.maintenance.MaintenanceVisit = frappe.ui.form.Controller.extend({
refresh: function () { refresh: function () {
frappe.dynamic_link = {doc: this.frm.doc, fieldname: 'customer', doctype: 'Customer'} frappe.dynamic_link = { doc: this.frm.doc, fieldname: 'customer', doctype: 'Customer' };
var me = this; var me = this;

View File

@ -4,6 +4,7 @@
from __future__ import unicode_literals from __future__ import unicode_literals
import frappe import frappe
from frappe import _ from frappe import _
from frappe.utils import get_datetime
from erpnext.utilities.transaction_base import TransactionBase from erpnext.utilities.transaction_base import TransactionBase
@ -16,10 +17,28 @@ class MaintenanceVisit(TransactionBase):
if d.serial_no and not frappe.db.exists("Serial No", d.serial_no): if d.serial_no and not frappe.db.exists("Serial No", d.serial_no):
frappe.throw(_("Serial No {0} does not exist").format(d.serial_no)) frappe.throw(_("Serial No {0} does not exist").format(d.serial_no))
def validate_maintenance_date(self):
if self.maintenance_type == "Scheduled" and self.maintenance_schedule_detail:
item_ref = frappe.db.get_value('Maintenance Schedule Detail', self.maintenance_schedule_detail, 'item_reference')
if item_ref:
start_date, end_date = frappe.db.get_value('Maintenance Schedule Item', item_ref, ['start_date', 'end_date'])
if get_datetime(self.mntc_date) < get_datetime(start_date) or get_datetime(self.mntc_date) > get_datetime(end_date):
frappe.throw(_("Date must be between {0} and {1}").format(start_date, end_date))
def validate(self): def validate(self):
self.validate_serial_no() self.validate_serial_no()
self.validate_maintenance_date()
def update_completion_status(self):
if self.maintenance_schedule_detail:
frappe.db.set_value('Maintenance Schedule Detail', self.maintenance_schedule_detail, 'completion_status', self.completion_status)
def update_actual_date(self):
if self.maintenance_schedule_detail:
frappe.db.set_value('Maintenance Schedule Detail', self.maintenance_schedule_detail, 'actual_date', self.mntc_date)
def update_customer_issue(self, flag): def update_customer_issue(self, flag):
if not self.maintenance_schedule:
for d in self.get('purposes'): for d in self.get('purposes'):
if d.prevdoc_docname and d.prevdoc_doctype == 'Warranty Claim' : if d.prevdoc_docname and d.prevdoc_doctype == 'Warranty Claim' :
if flag==1: if flag==1:
@ -77,6 +96,8 @@ class MaintenanceVisit(TransactionBase):
def on_submit(self): def on_submit(self):
self.update_customer_issue(1) self.update_customer_issue(1)
frappe.db.set(self, 'status', 'Submitted') frappe.db.set(self, 'status', 'Submitted')
self.update_completion_status()
self.update_actual_date()
def on_cancel(self): def on_cancel(self):
self.check_if_last_visit() self.check_if_last_visit()

View File

@ -1,4 +1,5 @@
{ {
"actions": [],
"autoname": "hash", "autoname": "hash",
"creation": "2013-02-22 01:28:06", "creation": "2013-02-22 01:28:06",
"doctype": "DocType", "doctype": "DocType",
@ -8,14 +9,15 @@
"field_order": [ "field_order": [
"item_code", "item_code",
"item_name", "item_name",
"column_break_3",
"service_person",
"serial_no", "serial_no",
"section_break_6",
"description", "description",
"work_details", "work_details",
"service_person",
"work_done", "work_done",
"prevdoc_doctype", "prevdoc_doctype",
"prevdoc_docname", "prevdoc_docname"
"prevdoc_detail_docname"
], ],
"fields": [ "fields": [
{ {
@ -62,6 +64,8 @@
"fieldtype": "Section Break" "fieldtype": "Section Break"
}, },
{ {
"fetch_from": "prevdoc_detail_docname.sales_person",
"fetch_if_empty": 1,
"fieldname": "service_person", "fieldname": "service_person",
"fieldtype": "Link", "fieldtype": "Link",
"in_list_view": 1, "in_list_view": 1,
@ -83,49 +87,30 @@
{ {
"fieldname": "prevdoc_doctype", "fieldname": "prevdoc_doctype",
"fieldtype": "Link", "fieldtype": "Link",
"hidden": 1,
"label": "Document Type", "label": "Document Type",
"no_copy": 1, "options": "DocType"
"oldfieldname": "prevdoc_doctype",
"oldfieldtype": "Data",
"options": "DocType",
"print_hide": 1,
"print_width": "150px",
"read_only": 1,
"report_hide": 1,
"width": "150px"
}, },
{ {
"fieldname": "prevdoc_docname", "fieldname": "prevdoc_docname",
"fieldtype": "Dynamic Link", "fieldtype": "Dynamic Link",
"hidden": 1,
"label": "Against Document No", "label": "Against Document No",
"no_copy": 1, "options": "prevdoc_doctype"
"oldfieldname": "prevdoc_docname",
"oldfieldtype": "Data",
"options": "prevdoc_doctype",
"print_hide": 1,
"print_width": "160px",
"read_only": 1,
"report_hide": 1,
"width": "160px"
}, },
{ {
"fieldname": "prevdoc_detail_docname", "fieldname": "column_break_3",
"fieldtype": "Data", "fieldtype": "Column Break"
"hidden": 1, },
"label": "Against Document Detail No", {
"no_copy": 1, "fieldname": "section_break_6",
"oldfieldname": "prevdoc_detail_docname", "fieldtype": "Section Break"
"oldfieldtype": "Data",
"print_hide": 1,
"print_width": "160px",
"read_only": 1,
"report_hide": 1,
"width": "160px"
} }
], ],
"idx": 1, "idx": 1,
"istable": 1, "istable": 1,
"modified": "2020-09-18 17:26:09.703215", "links": [],
"modified": "2021-05-27 17:47:21.474282",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Maintenance", "module": "Maintenance",
"name": "Maintenance Visit Purpose", "name": "Maintenance Visit Purpose",

View File

@ -433,6 +433,7 @@ def make_material_request(source_name, target_doc=None):
def make_stock_entry(source_name, target_doc=None): def make_stock_entry(source_name, target_doc=None):
def update_item(obj, target, source_parent): def update_item(obj, target, source_parent):
target.t_warehouse = source_parent.wip_warehouse target.t_warehouse = source_parent.wip_warehouse
if not target.conversion_factor:
target.conversion_factor = 1 target.conversion_factor = 1
def set_missing_values(source, target): def set_missing_values(source, target):

View File

@ -212,6 +212,8 @@ frappe.ui.form.on('Production Plan', {
}, },
get_items: function (frm) { get_items: function (frm) {
frm.clear_table('prod_plan_references');
frappe.call({ frappe.call({
method: "get_items", method: "get_items",
freeze: true, freeze: true,
@ -221,6 +223,15 @@ frappe.ui.form.on('Production Plan', {
} }
}); });
}, },
combine_items: function (frm) {
frm.clear_table('prod_plan_references');
frappe.call({
method: "get_items",
freeze: true,
doc: frm.doc,
});
},
get_items_for_mr: function(frm) { get_items_for_mr: function(frm) {
if (!frm.doc.for_warehouse) { if (!frm.doc.for_warehouse) {

View File

@ -28,7 +28,10 @@
"material_requests", "material_requests",
"select_items_to_manufacture_section", "select_items_to_manufacture_section",
"get_items", "get_items",
"combine_items",
"po_items", "po_items",
"section_break_25",
"prod_plan_references",
"material_request_planning", "material_request_planning",
"include_non_stock_items", "include_non_stock_items",
"include_subcontracted_items", "include_subcontracted_items",
@ -316,13 +319,31 @@
"fieldname": "include_safety_stock", "fieldname": "include_safety_stock",
"fieldtype": "Check", "fieldtype": "Check",
"label": "Include Safety Stock in Required Qty Calculation" "label": "Include Safety Stock in Required Qty Calculation"
},
{
"default": "0",
"depends_on": "eval:doc.get_items_from == 'Sales Order'",
"fieldname": "combine_items",
"fieldtype": "Check",
"label": "Consolidate Items"
},
{
"fieldname": "section_break_25",
"fieldtype": "Section Break"
},
{
"fieldname": "prod_plan_references",
"fieldtype": "Table",
"hidden": 1,
"label": "Production Plan Item Reference",
"options": "Production Plan Item Reference"
} }
], ],
"icon": "fa fa-calendar", "icon": "fa fa-calendar",
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2021-03-08 11:17:25.470147", "modified": "2021-05-24 16:59:03.643211",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Manufacturing", "module": "Manufacturing",
"name": "Production Plan", "name": "Production Plan",

View File

@ -96,8 +96,10 @@ class ProductionPlan(Document):
@frappe.whitelist() @frappe.whitelist()
def get_items(self): def get_items(self):
self.set('po_items', [])
if self.get_items_from == "Sales Order": if self.get_items_from == "Sales Order":
self.get_so_items() self.get_so_items()
elif self.get_items_from == "Material Request": elif self.get_items_from == "Material Request":
self.get_mr_items() self.get_mr_items()
@ -165,9 +167,31 @@ class ProductionPlan(Document):
self.calculate_total_planned_qty() self.calculate_total_planned_qty()
def add_items(self, items): def add_items(self, items):
self.set('po_items', []) refs = {}
for data in items: for data in items:
item_details = get_item_details(data.item_code) item_details = get_item_details(data.item_code)
if self.combine_items:
if item_details.bom_no in refs:
refs[item_details.bom_no]['so_details'].append({
'sales_order': data.parent,
'sales_order_item': data.name,
'qty': data.pending_qty
})
refs[item_details.bom_no]['qty'] += data.pending_qty
continue
else:
refs[item_details.bom_no] = {
'qty': data.pending_qty,
'po_item_ref': data.name,
'so_details': []
}
refs[item_details.bom_no]['so_details'].append({
'sales_order': data.parent,
'sales_order_item': data.name,
'qty': data.pending_qty
})
pi = self.append('po_items', { pi = self.append('po_items', {
'include_exploded_items': 1, 'include_exploded_items': 1,
'warehouse': data.warehouse, 'warehouse': data.warehouse,
@ -191,6 +215,23 @@ class ProductionPlan(Document):
pi.material_request_item = data.name pi.material_request_item = data.name
pi.description = data.description pi.description = data.description
if refs:
for po_item in self.po_items:
po_item.planned_qty = refs[po_item.bom_no]['qty']
po_item.pending_qty = refs[po_item.bom_no]['qty']
po_item.sales_order = ''
self.add_pp_ref(refs)
def add_pp_ref(self, refs):
for bom_no in refs:
for so_detail in refs[bom_no]['so_details']:
self.append('prod_plan_references', {
'item_reference': refs[bom_no]['po_item_ref'],
'sales_order': so_detail['sales_order'],
'sales_order_item': so_detail['sales_order_item'],
'qty': so_detail['qty']
})
def calculate_total_planned_qty(self): def calculate_total_planned_qty(self):
self.total_planned_qty = 0 self.total_planned_qty = 0
for d in self.po_items: for d in self.po_items:

View File

@ -100,7 +100,7 @@ class TestProductionPlan(unittest.TestCase):
def test_production_plan_sales_orders(self): def test_production_plan_sales_orders(self):
item = 'Test Production Item 1' item = 'Test Production Item 1'
so = make_sales_order(item_code=item, qty=5) so = make_sales_order(item_code=item, qty=1)
sales_order = so.name sales_order = so.name
sales_order_item = so.items[0].name sales_order_item = so.items[0].name
@ -124,8 +124,8 @@ class TestProductionPlan(unittest.TestCase):
wo_doc = frappe.get_doc('Work Order', work_order) wo_doc = frappe.get_doc('Work Order', work_order)
wo_doc.update({ wo_doc.update({
'wip_warehouse': '_Test Warehouse 1 - _TC', 'wip_warehouse': 'Work In Progress - _TC',
'fg_warehouse': '_Test Warehouse - _TC' 'fg_warehouse': 'Finished Goods - _TC'
}) })
wo_doc.submit() wo_doc.submit()
@ -145,6 +145,58 @@ class TestProductionPlan(unittest.TestCase):
self.assertEqual(sales_orders, []) self.assertEqual(sales_orders, [])
def test_production_plan_combine_items(self):
item = 'Test Production Item 1'
so = make_sales_order(item_code=item, qty=1)
pln = frappe.new_doc('Production Plan')
pln.company = so.company
pln.get_items_from = 'Sales Order'
pln.append('sales_orders', {
'sales_order': so.name,
'sales_order_date': so.transaction_date,
'customer': so.customer,
'grand_total': so.grand_total
})
so = make_sales_order(item_code=item, qty=2)
pln.append('sales_orders', {
'sales_order': so.name,
'sales_order_date': so.transaction_date,
'customer': so.customer,
'grand_total': so.grand_total
})
pln.combine_items = 1
pln.get_items()
pln.submit()
self.assertTrue(pln.po_items[0].planned_qty, 3)
pln.make_work_order()
work_order = frappe.db.get_value('Work Order', {
'production_plan_item': pln.po_items[0].name,
'production_plan': pln.name
}, 'name')
wo_doc = frappe.get_doc('Work Order', work_order)
wo_doc.update({
'wip_warehouse': 'Work In Progress - _TC',
})
wo_doc.submit()
so_items = []
for plan_reference in pln.prod_plan_references:
so_items.append(plan_reference.sales_order_item)
so_wo_qty = frappe.db.get_value('Sales Order Item', plan_reference.sales_order_item, 'work_order_qty')
self.assertEqual(so_wo_qty, plan_reference.qty)
wo_doc.cancel()
for so_item in so_items:
so_wo_qty = frappe.db.get_value('Sales Order Item', so_item, 'work_order_qty')
self.assertEqual(so_wo_qty, 0.0)
latest_plan = frappe.get_doc('Production Plan', pln.name)
latest_plan.cancel()
def test_pp_to_mr_customer_provided(self): def test_pp_to_mr_customer_provided(self):
#Material Request from Production Plan for Customer Provided #Material Request from Production Plan for Customer Provided
create_item('CUST-0987', is_customer_provided_item = 1, customer = '_Test Customer', is_purchase_item = 0) create_item('CUST-0987', is_customer_provided_item = 1, customer = '_Test Customer', is_purchase_item = 0)

View File

@ -1,792 +1,229 @@
{ {
"allow_copy": 0, "actions": [],
"allow_events_in_timeline": 0,
"allow_guest_to_view": 0,
"allow_import": 0,
"allow_rename": 0,
"autoname": "hash", "autoname": "hash",
"beta": 0,
"creation": "2013-02-22 01:27:49", "creation": "2013-02-22 01:27:49",
"custom": 0,
"docstatus": 0,
"doctype": "DocType", "doctype": "DocType",
"editable_grid": 1, "editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"include_exploded_items",
"item_code",
"bom_no",
"planned_qty",
"column_break_6",
"make_work_order_for_sub_assembly_items",
"warehouse",
"planned_start_date",
"section_break_9",
"pending_qty",
"ordered_qty",
"produced_qty",
"column_break_17",
"description",
"stock_uom",
"reference_section",
"sales_order",
"sales_order_item",
"column_break_19",
"material_request",
"material_request_item",
"product_bundle_item",
"item_reference"
],
"fields": [ "fields": [
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 2, "columns": 2,
"fetch_if_empty": 0, "default": "0",
"fieldname": "include_exploded_items", "fieldname": "include_exploded_items",
"fieldtype": "Check", "fieldtype": "Check",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1, "in_list_view": 1,
"in_standard_filter": 0, "label": "Include Exploded Items"
"label": "Include Exploded Items",
"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,
"translatable": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 2, "columns": 2,
"fetch_if_empty": 0,
"fieldname": "item_code", "fieldname": "item_code",
"fieldtype": "Link", "fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1, "in_list_view": 1,
"in_standard_filter": 0,
"label": "Item Code", "label": "Item Code",
"length": 0,
"no_copy": 0,
"oldfieldname": "item_code", "oldfieldname": "item_code",
"oldfieldtype": "Link", "oldfieldtype": "Link",
"options": "Item", "options": "Item",
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"print_width": "150px", "print_width": "150px",
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1, "reqd": 1,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0,
"width": "150px" "width": "150px"
}, },
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 2, "columns": 2,
"fetch_if_empty": 0,
"fieldname": "bom_no", "fieldname": "bom_no",
"fieldtype": "Link", "fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1, "in_list_view": 1,
"in_standard_filter": 0,
"label": "BOM No", "label": "BOM No",
"length": 0,
"no_copy": 0,
"oldfieldname": "bom_no", "oldfieldname": "bom_no",
"oldfieldtype": "Link", "oldfieldtype": "Link",
"options": "BOM", "options": "BOM",
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"print_width": "100px", "print_width": "100px",
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1, "reqd": 1,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0,
"width": "100px" "width": "100px"
}, },
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fetch_if_empty": 0,
"fieldname": "planned_qty", "fieldname": "planned_qty",
"fieldtype": "Float", "fieldtype": "Float",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1, "in_list_view": 1,
"in_standard_filter": 0,
"label": "Planned Qty", "label": "Planned Qty",
"length": 0,
"no_copy": 0,
"oldfieldname": "planned_qty", "oldfieldname": "planned_qty",
"oldfieldtype": "Currency", "oldfieldtype": "Currency",
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"print_width": "100px", "print_width": "100px",
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1, "reqd": 1,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0,
"width": "100px" "width": "100px"
}, },
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fetch_if_empty": 0,
"fieldname": "column_break_6", "fieldname": "column_break_6",
"fieldtype": "Column Break", "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,
"translatable": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0, "default": "0",
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"depends_on": "",
"description": "If enabled, system will create the work order for the exploded items against which BOM is available.", "description": "If enabled, system will create the work order for the exploded items against which BOM is available.",
"fetch_if_empty": 0,
"fieldname": "make_work_order_for_sub_assembly_items", "fieldname": "make_work_order_for_sub_assembly_items",
"fieldtype": "Check", "fieldtype": "Check",
"hidden": 0, "label": "Make Work Order for Sub Assembly Items"
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Make Work Order for Sub Assembly Items",
"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,
"translatable": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"description": "",
"fetch_if_empty": 0,
"fieldname": "warehouse", "fieldname": "warehouse",
"fieldtype": "Link", "fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1, "in_list_view": 1,
"in_standard_filter": 0,
"label": "For Warehouse", "label": "For Warehouse",
"length": 0, "options": "Warehouse"
"no_copy": 0,
"options": "Warehouse",
"permlevel": 0,
"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,
"translatable": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"default": "Today", "default": "Today",
"fetch_if_empty": 0,
"fieldname": "planned_start_date", "fieldname": "planned_start_date",
"fieldtype": "Datetime", "fieldtype": "Datetime",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1, "in_list_view": 1,
"in_standard_filter": 0,
"label": "Planned Start Date", "label": "Planned Start Date",
"length": 0, "reqd": 1
"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": 1,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fetch_if_empty": 0,
"fieldname": "section_break_9", "fieldname": "section_break_9",
"fieldtype": "Section Break", "fieldtype": "Section Break",
"hidden": 0, "label": "Quantity and Description"
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Quantity and Description",
"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,
"translatable": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"default": "0", "default": "0",
"fetch_if_empty": 0,
"fieldname": "pending_qty", "fieldname": "pending_qty",
"fieldtype": "Float", "fieldtype": "Float",
"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,
"label": "Pending Qty", "label": "Pending Qty",
"length": 0,
"no_copy": 0,
"oldfieldname": "prevdoc_reqd_qty", "oldfieldname": "prevdoc_reqd_qty",
"oldfieldtype": "Currency", "oldfieldtype": "Currency",
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"print_width": "100px", "print_width": "100px",
"read_only": 1, "read_only": 1,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0,
"width": "100px" "width": "100px"
}, },
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"default": "0", "default": "0",
"fetch_if_empty": 0,
"fieldname": "ordered_qty", "fieldname": "ordered_qty",
"fieldtype": "Float", "fieldtype": "Float",
"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,
"label": "Ordered Qty", "label": "Ordered Qty",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 1, "print_hide": 1,
"print_hide_if_no_value": 0, "read_only": 1
"read_only": 1,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"default": "0", "default": "0",
"fetch_if_empty": 0,
"fieldname": "produced_qty", "fieldname": "produced_qty",
"fieldtype": "Float", "fieldtype": "Float",
"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,
"label": "Produced Qty", "label": "Produced Qty",
"length": 0,
"no_copy": 1, "no_copy": 1,
"permlevel": 0, "read_only": 1
"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,
"translatable": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fetch_if_empty": 0,
"fieldname": "column_break_17", "fieldname": "column_break_17",
"fieldtype": "Column Break", "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,
"translatable": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fetch_if_empty": 0,
"fieldname": "description", "fieldname": "description",
"fieldtype": "Text Editor", "fieldtype": "Text Editor",
"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,
"label": "Description", "label": "Description",
"length": 0,
"no_copy": 0,
"oldfieldname": "description", "oldfieldname": "description",
"oldfieldtype": "Text", "oldfieldtype": "Text",
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"print_width": "200px", "print_width": "200px",
"read_only": 1, "read_only": 1,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0,
"width": "200px" "width": "200px"
}, },
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fetch_if_empty": 0,
"fieldname": "stock_uom", "fieldname": "stock_uom",
"fieldtype": "Link", "fieldtype": "Link",
"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,
"label": "UOM", "label": "UOM",
"length": 0,
"no_copy": 0,
"oldfieldname": "stock_uom", "oldfieldname": "stock_uom",
"oldfieldtype": "Data", "oldfieldtype": "Data",
"options": "UOM", "options": "UOM",
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"print_width": "80px", "print_width": "80px",
"read_only": 1, "read_only": 1,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1, "reqd": 1,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0,
"width": "80px" "width": "80px"
}, },
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fetch_if_empty": 0,
"fieldname": "reference_section", "fieldname": "reference_section",
"fieldtype": "Section Break", "fieldtype": "Section Break",
"hidden": 0, "label": "Reference"
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Reference",
"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,
"translatable": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fetch_if_empty": 0,
"fieldname": "sales_order", "fieldname": "sales_order",
"fieldtype": "Link", "fieldtype": "Link",
"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,
"label": "Sales Order", "label": "Sales Order",
"length": 0,
"no_copy": 0,
"oldfieldname": "source_docname", "oldfieldname": "source_docname",
"oldfieldtype": "Data", "oldfieldtype": "Data",
"options": "Sales Order", "options": "Sales Order",
"permlevel": 0, "read_only": 1
"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,
"translatable": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fetch_if_empty": 0,
"fieldname": "sales_order_item", "fieldname": "sales_order_item",
"fieldtype": "Data", "fieldtype": "Data",
"hidden": 1, "hidden": 1,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Sales Order Item", "label": "Sales Order Item",
"length": 0,
"no_copy": 1, "no_copy": 1,
"permlevel": 0, "print_hide": 1
"precision": "",
"print_hide": 1,
"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,
"translatable": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fetch_if_empty": 0,
"fieldname": "column_break_19", "fieldname": "column_break_19",
"fieldtype": "Column Break", "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,
"translatable": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fetch_if_empty": 0,
"fieldname": "material_request", "fieldname": "material_request",
"fieldtype": "Link", "fieldtype": "Link",
"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,
"label": "Material Request", "label": "Material Request",
"length": 0,
"no_copy": 0,
"options": "Material Request", "options": "Material Request",
"permlevel": 0, "read_only": 1
"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,
"translatable": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fetch_if_empty": 0,
"fieldname": "material_request_item", "fieldname": "material_request_item",
"fieldtype": "Data", "fieldtype": "Data",
"hidden": 1, "hidden": 1,
"ignore_user_permissions": 0, "label": "material_request_item"
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "material_request_item",
"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,
"translatable": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fetch_if_empty": 0,
"fieldname": "product_bundle_item", "fieldname": "product_bundle_item",
"fieldtype": "Link", "fieldtype": "Link",
"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,
"label": "Product Bundle Item", "label": "Product Bundle Item",
"length": 0,
"no_copy": 1, "no_copy": 1,
"options": "Item", "options": "Item",
"permlevel": 0,
"precision": "",
"print_hide": 1, "print_hide": 1,
"print_hide_if_no_value": 0, "read_only": 1
"read_only": 1, },
"remember_last_selected_value": 0, {
"report_hide": 0, "fieldname": "item_reference",
"reqd": 0, "fieldtype": "Data",
"search_index": 0, "hidden": 1,
"set_only_once": 0, "label": "Item Reference"
"translatable": 0,
"unique": 0
} }
], ],
"has_web_view": 0,
"hide_toolbar": 0,
"idx": 1, "idx": 1,
"in_create": 0,
"is_submittable": 0,
"issingle": 0,
"istable": 1, "istable": 1,
"max_attachments": 0, "links": [],
"modified": "2019-04-08 23:09:57.199423", "modified": "2021-04-28 19:14:57.772123",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Manufacturing", "module": "Manufacturing",
"name": "Production Plan Item", "name": "Production Plan Item",
"owner": "Administrator", "owner": "Administrator",
"permissions": [], "permissions": [],
"quick_entry": 0, "sort_field": "modified",
"read_only": 0, "sort_order": "ASC"
"show_name_in_global_search": 0,
"sort_order": "ASC",
"track_changes": 0,
"track_seen": 0,
"track_views": 0
} }

View File

@ -0,0 +1,52 @@
{
"actions": [],
"creation": "2021-04-22 10:32:58.896330",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"item_reference",
"sales_order",
"sales_order_item",
"qty"
],
"fields": [
{
"fieldname": "sales_order",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Sales Order Reference",
"options": "Sales Order"
},
{
"fieldname": "sales_order_item",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Sales Order Item"
},
{
"fieldname": "qty",
"fieldtype": "Data",
"in_list_view": 1,
"label": "qty"
},
{
"fieldname": "item_reference",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Item Reference"
}
],
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2021-05-07 17:03:49.707487",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "Production Plan Item Reference",
"owner": "Administrator",
"permissions": [],
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
}

View File

@ -0,0 +1,10 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2021, 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 ProductionPlanItemReference(Document):
pass

View File

@ -76,9 +76,9 @@ frappe.ui.form.on("Work Order", {
frm.set_query("production_item", function() { frm.set_query("production_item", function() {
return { return {
query: "erpnext.controllers.queries.item_query", query: "erpnext.controllers.queries.item_query",
filters:[ filters: {
['is_stock_item', '=',1] "is_stock_item": 1,
] }
}; };
}); });

View File

@ -241,7 +241,11 @@ class WorkOrder(Document):
if not self.fg_warehouse: if not self.fg_warehouse:
frappe.throw(_("For Warehouse is required before Submit")) frappe.throw(_("For Warehouse is required before Submit"))
if self.production_plan and frappe.db.exists('Production Plan Item Reference',{'parent':self.production_plan}):
self.update_work_order_qty_in_combined_so()
else:
self.update_work_order_qty_in_so() self.update_work_order_qty_in_so()
self.update_reserved_qty_for_production() self.update_reserved_qty_for_production()
self.update_completed_qty_in_material_request() self.update_completed_qty_in_material_request()
self.update_planned_qty() self.update_planned_qty()
@ -250,9 +254,13 @@ class WorkOrder(Document):
def on_cancel(self): def on_cancel(self):
self.validate_cancel() self.validate_cancel()
frappe.db.set(self,'status', 'Cancelled') frappe.db.set(self,'status', 'Cancelled')
if self.production_plan and frappe.db.exists('Production Plan Item Reference',{'parent':self.production_plan}):
self.update_work_order_qty_in_combined_so()
else:
self.update_work_order_qty_in_so() self.update_work_order_qty_in_so()
self.delete_job_card() self.delete_job_card()
self.update_completed_qty_in_material_request() self.update_completed_qty_in_material_request()
self.update_planned_qty() self.update_planned_qty()
@ -358,6 +366,27 @@ class WorkOrder(Document):
frappe.db.set_value('Sales Order Item', frappe.db.set_value('Sales Order Item',
self.sales_order_item, 'work_order_qty', flt(work_order_qty/total_bundle_qty, 2)) self.sales_order_item, 'work_order_qty', flt(work_order_qty/total_bundle_qty, 2))
def update_work_order_qty_in_combined_so(self):
total_bundle_qty = 1
if self.product_bundle_item:
total_bundle_qty = frappe.db.sql(""" select sum(qty) from
`tabProduct Bundle Item` where parent = %s""", (frappe.db.escape(self.product_bundle_item)))[0][0]
if not total_bundle_qty:
# product bundle is 0 (product bundle allows 0 qty for items)
total_bundle_qty = 1
prod_plan = frappe.get_doc('Production Plan', self.production_plan)
item_reference = frappe.get_value('Production Plan Item', self.production_plan_item, 'sales_order_item')
for plan_reference in prod_plan.prod_plan_references:
work_order_qty = 0.0
if plan_reference.item_reference == item_reference:
if self.docstatus == 1:
work_order_qty = flt(plan_reference.qty) / total_bundle_qty
frappe.db.set_value('Sales Order Item',
plan_reference.sales_order_item, 'work_order_qty', work_order_qty)
def update_completed_qty_in_material_request(self): def update_completed_qty_in_material_request(self):
if self.material_request: if self.material_request:
frappe.get_doc("Material Request", self.material_request).update_completed_qty([self.material_request_item]) frappe.get_doc("Material Request", self.material_request).update_completed_qty([self.material_request_item])

View File

@ -779,5 +779,7 @@ erpnext.patches.v12_0.add_ewaybill_validity_field
erpnext.patches.v13_0.germany_make_custom_fields erpnext.patches.v13_0.germany_make_custom_fields
erpnext.patches.v13_0.germany_fill_debtor_creditor_number erpnext.patches.v13_0.germany_fill_debtor_creditor_number
erpnext.patches.v13_0.set_pos_closing_as_failed erpnext.patches.v13_0.set_pos_closing_as_failed
execute:frappe.rename_doc("Workspace", "Loan Management", "Loans", force=True)
erpnext.patches.v13_0.update_timesheet_changes erpnext.patches.v13_0.update_timesheet_changes
erpnext.patches.v13_0.set_training_event_attendance erpnext.patches.v13_0.set_training_event_attendance
erpnext.patches.v13_0.rename_issue_status_hold_to_on_hold

View File

@ -19,6 +19,9 @@ def execute():
logger.info("purchase_receipt_status: begin patch, PR count: {}" logger.info("purchase_receipt_status: begin patch, PR count: {}"
.format(len(affected_purchase_receipts))) .format(len(affected_purchase_receipts)))
frappe.reload_doc("stock", "doctype", "Purchase Receipt")
frappe.reload_doc("stock", "doctype", "Purchase Receipt Item")
for pr in affected_purchase_receipts: for pr in affected_purchase_receipts:
pr_name = pr[0] pr_name = pr[0]

View File

@ -0,0 +1,20 @@
# Copyright (c) 2020, Frappe and Contributors
# License: GNU General Public License v3. See license.txt
from __future__ import unicode_literals
import frappe
def execute():
if frappe.db.exists('DocType', 'Issue'):
frappe.reload_doc("support", "doctype", "issue")
rename_status()
def rename_status():
frappe.db.sql("""
UPDATE
`tabIssue`
SET
status = 'On Hold'
WHERE
status = 'Hold'
""")

View File

@ -15,7 +15,13 @@ from erpnext.payroll.doctype.salary_structure.test_salary_structure import make_
from erpnext.loan_management.doctype.loan.test_loan import create_loan, make_loan_disbursement_entry, create_loan_type, create_loan_accounts from erpnext.loan_management.doctype.loan.test_loan import create_loan, make_loan_disbursement_entry, create_loan_type, create_loan_accounts
from erpnext.loan_management.doctype.process_loan_interest_accrual.process_loan_interest_accrual import process_loan_interest_accrual_for_term_loans from erpnext.loan_management.doctype.process_loan_interest_accrual.process_loan_interest_accrual import process_loan_interest_accrual_for_term_loans
test_dependencies = ['Holiday List']
class TestPayrollEntry(unittest.TestCase): class TestPayrollEntry(unittest.TestCase):
@classmethod
def setUpClass(cls):
frappe.db.set_value("Company", erpnext.get_default_company(), "default_holiday_list", '_Test Holiday List')
def setUp(self): def setUp(self):
for dt in ["Salary Slip", "Salary Component", "Salary Component Account", for dt in ["Salary Slip", "Salary Component", "Salary Component Account",
"Payroll Entry", "Salary Structure", "Salary Structure Assignment", "Payroll Employee Detail", "Additional Salary"]: "Payroll Entry", "Salary Structure", "Salary Structure Assignment", "Payroll Employee Detail", "Additional Salary"]:

View File

@ -115,10 +115,23 @@ class SalarySlip(TransactionBase):
status = "Cancelled" status = "Cancelled"
return status return status
def validate_dates(self): def validate_dates(self, joining_date=None, relieving_date=None):
if date_diff(self.end_date, self.start_date) < 0: if date_diff(self.end_date, self.start_date) < 0:
frappe.throw(_("To date cannot be before From date")) frappe.throw(_("To date cannot be before From date"))
if not joining_date:
joining_date, relieving_date = frappe.get_cached_value(
"Employee",
self.employee,
("date_of_joining", "relieving_date")
)
if date_diff(self.end_date, joining_date) < 0:
frappe.throw(_("Cannot create Salary Slip for Employee joining after Payroll Period"))
if relieving_date and date_diff(relieving_date, self.start_date) < 0:
frappe.throw(_("Cannot create Salary Slip for Employee who has left before Payroll Period"))
def is_rounding_total_disabled(self): def is_rounding_total_disabled(self):
return cint(frappe.db.get_single_value("Payroll Settings", "disable_rounded_total")) return cint(frappe.db.get_single_value("Payroll Settings", "disable_rounded_total"))
@ -154,9 +167,14 @@ class SalarySlip(TransactionBase):
if not self.salary_slip_based_on_timesheet: if not self.salary_slip_based_on_timesheet:
self.get_date_details() self.get_date_details()
self.validate_dates()
joining_date, relieving_date = frappe.get_cached_value("Employee", self.employee, joining_date, relieving_date = frappe.get_cached_value(
["date_of_joining", "relieving_date"]) "Employee",
self.employee,
("date_of_joining", "relieving_date")
)
self.validate_dates(joining_date, relieving_date)
#getin leave details #getin leave details
self.get_working_days_details(joining_date, relieving_date) self.get_working_days_details(joining_date, relieving_date)
@ -492,11 +510,39 @@ class SalarySlip(TransactionBase):
def get_data_for_eval(self): def get_data_for_eval(self):
'''Returns data for evaluating formula''' '''Returns data for evaluating formula'''
data = frappe._dict() data = frappe._dict()
employee = frappe.get_doc("Employee", self.employee).as_dict()
data.update(frappe.get_doc("Salary Structure Assignment", start_date = getdate(self.start_date)
{"employee": self.employee, "salary_structure": self.salary_structure}).as_dict()) date_to_validate = (
employee.date_of_joining
if employee.date_of_joining > start_date
else start_date
)
data.update(frappe.get_doc("Employee", self.employee).as_dict()) salary_structure_assignment = frappe.get_value(
"Salary Structure Assignment",
{
"employee": self.employee,
"salary_structure": self.salary_structure,
"from_date": ("<=", date_to_validate),
"docstatus": 1,
},
"*",
order_by="from_date desc",
as_dict=True,
)
if not salary_structure_assignment:
frappe.throw(
_("Please assign a Salary Structure for Employee {0} "
"applicable from or before {1} first").format(
frappe.bold(self.employee_name),
frappe.bold(formatdate(date_to_validate)),
)
)
data.update(salary_structure_assignment)
data.update(employee)
data.update(self.as_dict()) data.update(self.as_dict())
# set values for components # set values for components

View File

@ -8,7 +8,6 @@ import erpnext
import calendar import calendar
import random import random
from erpnext.accounts.utils import get_fiscal_year from erpnext.accounts.utils import get_fiscal_year
from frappe.utils.make_random import get_random
from frappe.utils import getdate, nowdate, add_days, add_months, flt, get_first_day, get_last_day, cstr from frappe.utils import getdate, nowdate, add_days, add_months, flt, get_first_day, get_last_day, cstr
from erpnext.payroll.doctype.salary_structure.salary_structure import make_salary_slip from erpnext.payroll.doctype.salary_structure.salary_structure import make_salary_slip
from erpnext.payroll.doctype.payroll_entry.payroll_entry import get_month_details from erpnext.payroll.doctype.payroll_entry.payroll_entry import get_month_details
@ -155,12 +154,14 @@ class TestSalarySlip(unittest.TestCase):
self.assertEqual(ss.gross_pay, 78000) self.assertEqual(ss.gross_pay, 78000)
def test_payment_days(self): def test_payment_days(self):
from erpnext.payroll.doctype.salary_structure.test_salary_structure import create_salary_structure_assignment
no_of_days = self.get_no_of_days() no_of_days = self.get_no_of_days()
# Holidays not included in working days # Holidays not included in working days
frappe.db.set_value("Payroll Settings", None, "include_holidays_in_total_working_days", 1) frappe.db.set_value("Payroll Settings", None, "include_holidays_in_total_working_days", 1)
# set joinng date in the same month # set joinng date in the same month
make_employee("test_payment_days@salary.com") employee = make_employee("test_payment_days@salary.com")
if getdate(nowdate()).day >= 15: if getdate(nowdate()).day >= 15:
relieving_date = getdate(add_days(nowdate(),-10)) relieving_date = getdate(add_days(nowdate(),-10))
date_of_joining = getdate(add_days(nowdate(),-10)) date_of_joining = getdate(add_days(nowdate(),-10))
@ -174,25 +175,30 @@ class TestSalarySlip(unittest.TestCase):
date_of_joining = getdate(nowdate()) date_of_joining = getdate(nowdate())
relieving_date = getdate(nowdate()) relieving_date = getdate(nowdate())
frappe.db.set_value("Employee", frappe.get_value("Employee", frappe.db.set_value("Employee", employee, {
{"employee_name":"test_payment_days@salary.com"}, "name"), "date_of_joining", date_of_joining) "date_of_joining": date_of_joining,
frappe.db.set_value("Employee", frappe.get_value("Employee", "relieving_date": None,
{"employee_name":"test_payment_days@salary.com"}, "name"), "relieving_date", None) "status": "Active"
frappe.db.set_value("Employee", frappe.get_value("Employee", })
{"employee_name":"test_payment_days@salary.com"}, "name"), "status", "Active")
ss = make_employee_salary_slip("test_payment_days@salary.com", "Monthly", "Test Payment Days") salary_structure = "Test Payment Days"
ss = make_employee_salary_slip("test_payment_days@salary.com", "Monthly", salary_structure)
self.assertEqual(ss.total_working_days, no_of_days[0]) self.assertEqual(ss.total_working_days, no_of_days[0])
self.assertEqual(ss.payment_days, (no_of_days[0] - getdate(date_of_joining).day + 1)) self.assertEqual(ss.payment_days, (no_of_days[0] - getdate(date_of_joining).day + 1))
# set relieving date in the same month # set relieving date in the same month
frappe.db.set_value("Employee",frappe.get_value("Employee", frappe.db.set_value("Employee", employee, {
{"employee_name":"test_payment_days@salary.com"}, "name"), "date_of_joining", (add_days(nowdate(),-60))) "date_of_joining": add_days(nowdate(),-60),
frappe.db.set_value("Employee", frappe.get_value("Employee", "relieving_date": relieving_date,
{"employee_name":"test_payment_days@salary.com"}, "name"), "relieving_date", relieving_date) "status": "Left"
frappe.db.set_value("Employee", frappe.get_value("Employee", })
{"employee_name":"test_payment_days@salary.com"}, "name"), "status", "Left")
if date_of_joining.day > 1:
self.assertRaises(frappe.ValidationError, ss.save)
create_salary_structure_assignment(employee, salary_structure)
ss.reload()
ss.save() ss.save()
self.assertEqual(ss.total_working_days, no_of_days[0]) self.assertEqual(ss.total_working_days, no_of_days[0])
@ -285,6 +291,7 @@ class TestSalarySlip(unittest.TestCase):
def test_multi_currency_salary_slip(self): def test_multi_currency_salary_slip(self):
from erpnext.payroll.doctype.salary_structure.test_salary_structure import make_salary_structure from erpnext.payroll.doctype.salary_structure.test_salary_structure import make_salary_structure
applicant = make_employee("test_multi_currency_salary_slip@salary.com", company="_Test Company") applicant = make_employee("test_multi_currency_salary_slip@salary.com", company="_Test Company")
frappe.db.sql("""delete from `tabSalary Structure` where name='Test Multi Currency Salary Slip'""") frappe.db.sql("""delete from `tabSalary Structure` where name='Test Multi Currency Salary Slip'""")
salary_structure = make_salary_structure("Test Multi Currency Salary Slip", "Monthly", employee=applicant, company="_Test Company", currency='USD') salary_structure = make_salary_structure("Test Multi Currency Salary Slip", "Monthly", employee=applicant, company="_Test Company", currency='USD')
@ -325,7 +332,8 @@ class TestSalarySlip(unittest.TestCase):
def test_component_wise_year_to_date_computation(self): def test_component_wise_year_to_date_computation(self):
from erpnext.payroll.doctype.salary_structure.test_salary_structure import make_salary_structure from erpnext.payroll.doctype.salary_structure.test_salary_structure import make_salary_structure
applicant = make_employee("test_ytd@salary.com", company="_Test Company") employee_name = "test_component_wise_ytd@salary.com"
applicant = make_employee(employee_name, company="_Test Company")
payroll_period = create_payroll_period(name="_Test Payroll Period 1", company="_Test Company") payroll_period = create_payroll_period(name="_Test Payroll Period 1", company="_Test Company")
@ -336,13 +344,13 @@ class TestSalarySlip(unittest.TestCase):
"Monthly", employee=applicant, company="_Test Company", currency="INR", payroll_period=payroll_period) "Monthly", employee=applicant, company="_Test Company", currency="INR", payroll_period=payroll_period)
# clear salary slip for this employee # clear salary slip for this employee
frappe.db.sql("DELETE FROM `tabSalary Slip` where employee_name = 'test_ytd@salary.com'") frappe.db.sql("DELETE FROM `tabSalary Slip` where employee_name = '%s'" % employee_name)
create_salary_slips_for_payroll_period(applicant, salary_structure.name, create_salary_slips_for_payroll_period(applicant, salary_structure.name,
payroll_period, deduct_random=False, num=3) payroll_period, deduct_random=False, num=3)
salary_slips = frappe.get_all("Salary Slip", fields=["name"], filters={"employee_name": salary_slips = frappe.get_all("Salary Slip", fields=["name"], filters={"employee_name":
"test_ytd@salary.com"}, order_by = "posting_date") employee_name}, order_by="posting_date")
year_to_date = dict() year_to_date = dict()
for slip in salary_slips: for slip in salary_slips:
@ -380,10 +388,10 @@ class TestSalarySlip(unittest.TestCase):
from erpnext.payroll.doctype.salary_structure.test_salary_structure import \ from erpnext.payroll.doctype.salary_structure.test_salary_structure import \
make_salary_structure, create_salary_structure_assignment make_salary_structure, create_salary_structure_assignment
salary_structure = make_salary_structure("Stucture to test tax", "Monthly", salary_structure = make_salary_structure("Stucture to test tax", "Monthly",
other_details={"max_benefits": 100000}, test_tax=True) other_details={"max_benefits": 100000}, test_tax=True,
create_salary_structure_assignment(employee, salary_structure.name, employee=employee, payroll_period=payroll_period)
payroll_period.start_date)
# create salary slip for whole period deducting tax only on last period # create salary slip for whole period deducting tax only on last period
# to find the total tax amount paid # to find the total tax amount paid
@ -469,6 +477,7 @@ class TestSalarySlip(unittest.TestCase):
def make_employee_salary_slip(user, payroll_frequency, salary_structure=None): def make_employee_salary_slip(user, payroll_frequency, salary_structure=None):
from erpnext.payroll.doctype.salary_structure.test_salary_structure import make_salary_structure from erpnext.payroll.doctype.salary_structure.test_salary_structure import make_salary_structure
if not salary_structure: if not salary_structure:
salary_structure = payroll_frequency + " Salary Structure Test for Salary Slip" salary_structure = payroll_frequency + " Salary Structure Test for Salary Slip"

View File

@ -6,7 +6,7 @@ import frappe
import unittest import unittest
import erpnext import erpnext
from frappe.utils.make_random import get_random from frappe.utils.make_random import get_random
from frappe.utils import nowdate, add_days, add_years, getdate, add_months from frappe.utils import nowdate, add_years, get_first_day, date_diff
from erpnext.payroll.doctype.salary_structure.salary_structure import make_salary_slip from erpnext.payroll.doctype.salary_structure.salary_structure import make_salary_slip
from erpnext.payroll.doctype.salary_slip.test_salary_slip import make_earning_salary_component,\ from erpnext.payroll.doctype.salary_slip.test_salary_slip import make_earning_salary_component,\
make_deduction_salary_component, make_employee_salary_slip, create_tax_slab make_deduction_salary_component, make_employee_salary_slip, create_tax_slab
@ -113,8 +113,9 @@ class TestSalaryStructure(unittest.TestCase):
sal_struct = make_salary_structure("Salary Structure Multi Currency", "Monthly", currency='USD') sal_struct = make_salary_structure("Salary Structure Multi Currency", "Monthly", currency='USD')
self.assertEqual(sal_struct.currency, 'USD') self.assertEqual(sal_struct.currency, 'USD')
def make_salary_structure(salary_structure, payroll_frequency, employee=None, dont_submit=False, other_details=None, def make_salary_structure(salary_structure, payroll_frequency, employee=None,
test_tax=False, company=None, currency=erpnext.get_default_currency(), payroll_period=None): from_date=None, dont_submit=False, other_details=None,test_tax=False,
company=None, currency=erpnext.get_default_currency(), payroll_period=None):
if test_tax: if test_tax:
frappe.db.sql("""delete from `tabSalary Structure` where name=%s""",(salary_structure)) frappe.db.sql("""delete from `tabSalary Structure` where name=%s""",(salary_structure))
@ -139,10 +140,23 @@ def make_salary_structure(salary_structure, payroll_frequency, employee=None, do
else: else:
salary_structure_doc = frappe.get_doc("Salary Structure", salary_structure) salary_structure_doc = frappe.get_doc("Salary Structure", salary_structure)
filters = {'employee':employee, 'docstatus': 1}
if not from_date and payroll_period:
from_date = payroll_period.start_date
if from_date:
filters['from_date'] = from_date
if employee and not frappe.db.get_value("Salary Structure Assignment", if employee and not frappe.db.get_value("Salary Structure Assignment",
{'employee':employee, 'docstatus': 1}) and salary_structure_doc.docstatus==1: filters) and salary_structure_doc.docstatus==1:
create_salary_structure_assignment(employee, salary_structure, company=company, currency=currency, create_salary_structure_assignment(
payroll_period=payroll_period) employee,
salary_structure,
from_date=from_date,
company=company,
currency=currency,
payroll_period=payroll_period
)
return salary_structure_doc return salary_structure_doc
@ -165,12 +179,13 @@ def create_salary_structure_assignment(employee, salary_structure, from_date=Non
salary_structure_assignment.base = 50000 salary_structure_assignment.base = 50000
salary_structure_assignment.variable = 5000 salary_structure_assignment.variable = 5000
if getdate(nowdate()).day == 1: if not from_date:
date = from_date or nowdate() from_date = get_first_day(nowdate())
else: joining_date = frappe.get_cached_value("Employee", employee, "date_of_joining")
date = from_date or add_days(nowdate(), -1) if date_diff(joining_date, from_date) > 0:
from_date = joining_date
salary_structure_assignment.from_date = date salary_structure_assignment.from_date = from_date
salary_structure_assignment.salary_structure = salary_structure salary_structure_assignment.salary_structure = salary_structure
salary_structure_assignment.currency = currency salary_structure_assignment.currency = currency
salary_structure_assignment.payroll_payable_account = get_payable_account(company) salary_structure_assignment.payroll_payable_account = get_payable_account(company)

View File

@ -261,11 +261,19 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({
if(!in_list(["Delivery Note", "Sales Invoice", "Purchase Receipt", "Purchase Invoice"], this.frm.doc.doctype)) { if(!in_list(["Delivery Note", "Sales Invoice", "Purchase Receipt", "Purchase Invoice"], this.frm.doc.doctype)) {
return; return;
} }
var me = this;
var inspection_type = in_list(["Purchase Receipt", "Purchase Invoice"], this.frm.doc.doctype) const me = this;
if (!this.frm.is_new() && this.frm.doc.docstatus === 0) {
this.frm.add_custom_button(__("Quality Inspection(s)"), () => {
me.make_quality_inspection();
}, __("Create"));
this.frm.page.set_inner_btn_group_as_primary(__('Create'));
}
const inspection_type = in_list(["Purchase Receipt", "Purchase Invoice"], this.frm.doc.doctype)
? "Incoming" : "Outgoing"; ? "Incoming" : "Outgoing";
var quality_inspection_field = this.frm.get_docfield("items", "quality_inspection"); let quality_inspection_field = this.frm.get_docfield("items", "quality_inspection");
quality_inspection_field.get_route_options_for_new_doc = function(row) { quality_inspection_field.get_route_options_for_new_doc = function(row) {
if(me.frm.is_new()) return; if(me.frm.is_new()) return;
return { return {
@ -280,7 +288,7 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({
} }
this.frm.set_query("quality_inspection", "items", function(doc, cdt, cdn) { this.frm.set_query("quality_inspection", "items", function(doc, cdt, cdn) {
var d = locals[cdt][cdn]; let d = locals[cdt][cdn];
return { return {
filters: { filters: {
docstatus: 1, docstatus: 1,
@ -1949,6 +1957,130 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({
}); });
}, },
make_quality_inspection: function () {
let data = [];
const fields = [
{
label: "Items",
fieldtype: "Table",
fieldname: "items",
cannot_add_rows: true,
in_place_edit: true,
data: data,
get_data: () => {
return data;
},
fields: [
{
fieldtype: "Data",
fieldname: "docname",
hidden: true
},
{
fieldtype: "Read Only",
fieldname: "item_code",
label: __("Item Code"),
in_list_view: true
},
{
fieldtype: "Read Only",
fieldname: "item_name",
label: __("Item Name"),
in_list_view: true
},
{
fieldtype: "Float",
fieldname: "qty",
label: __("Accepted Quantity"),
in_list_view: true,
read_only: true
},
{
fieldtype: "Float",
fieldname: "sample_size",
label: __("Sample Size"),
reqd: true,
in_list_view: true
},
{
fieldtype: "Data",
fieldname: "description",
label: __("Description"),
hidden: true
},
{
fieldtype: "Data",
fieldname: "serial_no",
label: __("Serial No"),
hidden: true
},
{
fieldtype: "Data",
fieldname: "batch_no",
label: __("Batch No"),
hidden: true
}
]
}
];
const me = this;
const dialog = new frappe.ui.Dialog({
title: __("Select Items for Quality Inspection"),
fields: fields,
primary_action: function () {
const data = dialog.get_values();
frappe.call({
method: "erpnext.controllers.stock_controller.make_quality_inspections",
args: {
doctype: me.frm.doc.doctype,
docname: me.frm.doc.name,
items: data.items
},
freeze: true,
callback: function (r) {
if (r.message.length > 0) {
if (r.message.length === 1) {
frappe.set_route("Form", "Quality Inspection", r.message[0]);
} else {
frappe.route_options = {
"reference_type": me.frm.doc.doctype,
"reference_name": me.frm.doc.name
};
frappe.set_route("List", "Quality Inspection");
}
}
dialog.hide();
}
});
},
primary_action_label: __("Create")
});
this.frm.doc.items.forEach(item => {
if (!item.quality_inspection) {
let dialog_items = dialog.fields_dict.items;
dialog_items.df.data.push({
"docname": item.name,
"item_code": item.item_code,
"item_name": item.item_name,
"qty": item.qty,
"description": item.description,
"serial_no": item.serial_no,
"batch_no": item.batch_no
});
dialog_items.grid.refresh();
}
});
data = dialog.fields_dict.items.df.data;
if (!data.length) {
frappe.msgprint(__("All items in this document already have a linked Quality Inspection."));
} else {
dialog.show();
}
},
get_method_for_payment: function(){ get_method_for_payment: function(){
var method = "erpnext.accounts.doctype.payment_entry.payment_entry.get_payment_entry"; var method = "erpnext.accounts.doctype.payment_entry.payment_entry.get_payment_entry";
if(cur_frm.doc.__onload && cur_frm.doc.__onload.make_payment_via_journal_entry){ if(cur_frm.doc.__onload && cur_frm.doc.__onload.make_payment_via_journal_entry){

View File

@ -20,10 +20,8 @@ class Quiz {
} }
make(data) { make(data) {
if (data.duration) { if (data.is_time_bound) {
const timer_display = document.createElement("div"); $(".lms-timer").removeClass("hide");
timer_display.classList.add("lms-timer", "float-right", "font-weight-bold");
document.getElementsByClassName("lms-title")[0].appendChild(timer_display);
if (!data.activity || (data.activity && !data.activity.is_complete)) { if (!data.activity || (data.activity && !data.activity.is_complete)) {
this.initialiseTimer(data.duration); this.initialiseTimer(data.duration);
this.is_time_bound = true; this.is_time_bound = true;
@ -118,7 +116,7 @@ class Quiz {
quiz_response: this.get_selected(), quiz_response: this.get_selected(),
course: this.course, course: this.course,
program: this.program, program: this.program,
time_taken: this.is_time_bound ? this.time_taken : "" time_taken: this.is_time_bound ? this.time_taken : 0
}).then(res => { }).then(res => {
this.submit_btn.remove() this.submit_btn.remove()
if (!res.message) { if (!res.message) {

View File

@ -1,4 +1,3 @@
@import "frappe/public/scss/desk/variables";
@import "frappe/public/scss/common/mixins"; @import "frappe/public/scss/common/mixins";
body.product-page { body.product-page {
@ -74,15 +73,6 @@ body.product-page {
} }
} }
// .card-body {
// text-align: center;
// }
// .featured-item {
// .card-body {
// text-align: left;
// }
// }
.card-img-container { .card-img-container {
height: 210px; height: 210px;
width: 100%; width: 100%;
@ -217,12 +207,12 @@ body.product-page {
border-color: var(--table-border-color) !important; border-color: var(--table-border-color) !important;
padding: 15px; padding: 15px;
@include media-breakpoint-between(xs, md) { @media (max-width: var(--md-width)) {
height: 300px; height: 300px;
width: 300px; width: 300px;
} }
@include media-breakpoint-up(lg) { @media (min-width: var(--lg-width)) {
height: 350px; height: 350px;
width: 350px; width: 350px;
} }
@ -233,11 +223,12 @@ body.product-page {
} }
.item-slideshow { .item-slideshow {
@include media-breakpoint-between(xs, md) {
@media (max-width: var(--md-width)) {
max-height: 320px; max-height: 320px;
} }
@include media-breakpoint-up(lg) { @media (min-width: var(--lg-width)) {
max-height: 430px; max-height: 430px;
} }
@ -254,7 +245,7 @@ body.product-page {
cursor: pointer; cursor: pointer;
&:hover, &.active { &:hover, &.active {
border-color: $primary; border-color: var(--primary);
} }
} }
@ -316,12 +307,9 @@ body.product-page {
} }
.item-group-slideshow { .item-group-slideshow {
.item-group-description {
// max-width: 900px;
}
.carousel-inner.rounded-carousel { .carousel-inner.rounded-carousel {
border-radius: $card-border-radius; border-radius: var(--card-border-radius);
} }
} }

View File

@ -1,4 +1,3 @@
@import "frappe/public/scss/website/variables";
.filter-options { .filter-options {
max-height: 300px; max-height: 300px;
@ -14,7 +13,7 @@
} }
&.active { &.active {
border-color: $primary; border-color: var(--primary);
.check { .check {
display: inline-flex; display: inline-flex;
@ -25,7 +24,7 @@
.check { .check {
display: inline-flex; display: inline-flex;
padding: 0.25rem; padding: 0.25rem;
background: $primary; background: var(--primary);
color: white; color: white;
border-radius: 50%; border-radius: 50%;
font-size: 12px; font-size: 12px;
@ -38,12 +37,12 @@
} }
.result { .result {
border-bottom: 1px solid $border-color; border-bottom: 1px solid var(--border-color);
} }
.transaction-list-item { .transaction-list-item {
padding: 1rem 0; padding: 1rem 0;
border-top: 1px solid $border-color; border-top: 1px solid var(--border-color);
position: relative; position: relative;
a.transaction-item-link { a.transaction-item-link {

View File

@ -310,7 +310,7 @@ class GSTR3BReport(Document):
self.report_dict['sup_details']['osup_det']['txval'] += taxable_value self.report_dict['sup_details']['osup_det']['txval'] += taxable_value
gst_category = self.invoice_detail_map.get(inv, {}).get('gst_category') gst_category = self.invoice_detail_map.get(inv, {}).get('gst_category')
place_of_supply = self.invoice_detail_map.get(inv, {}).get('place_of_supply', '00-Other Territory') place_of_supply = self.invoice_detail_map.get(inv, {}).get('place_of_supply') or '00-Other Territory'
if gst_category in ['Unregistered', 'Registered Composition', 'UIN Holders'] and \ if gst_category in ['Unregistered', 'Registered Composition', 'UIN Holders'] and \
self.gst_details.get("gst_state") != place_of_supply.split("-")[1]: self.gst_details.get("gst_state") != place_of_supply.split("-")[1]:

View File

@ -194,7 +194,7 @@ def get_item_list(invoice):
item.batch_expiry_date = frappe.db.get_value('Batch', d.batch_no, 'expiry_date') if d.batch_no else None item.batch_expiry_date = frappe.db.get_value('Batch', d.batch_no, 'expiry_date') if d.batch_no else None
item.batch_expiry_date = format_date(item.batch_expiry_date, 'dd/mm/yyyy') if item.batch_expiry_date else None item.batch_expiry_date = format_date(item.batch_expiry_date, 'dd/mm/yyyy') if item.batch_expiry_date else None
item.is_service_item = 'N' if frappe.db.get_value('Item', d.item_code, 'is_stock_item') else 'Y' item.is_service_item = 'Y' if item.gst_hsn_code[:2] == "99" else 'N'
item.serial_no = "" item.serial_no = ""
item = update_item_taxes(invoice, item) item = update_item_taxes(invoice, item)

View File

@ -500,7 +500,7 @@ def download_ewb_json():
if not isinstance(docname, list): if not isinstance(docname, list):
# removes characters not allowed in a filename (https://stackoverflow.com/a/38766141/4767738) # removes characters not allowed in a filename (https://stackoverflow.com/a/38766141/4767738)
filename_prefix = re.sub('[^\w_.)( -]', '', docname) filename_prefix = re.sub(r'[^\w_.)( -]', '', docname)
frappe.local.response.filename = '{0}_e-WayBill_Data_{1}.json'.format(filename_prefix, frappe.utils.random_string(5)) frappe.local.response.filename = '{0}_e-WayBill_Data_{1}.json'.format(filename_prefix, frappe.utils.random_string(5))

View File

@ -147,6 +147,13 @@ class Gstr1Report(object):
def get_invoice_data(self): def get_invoice_data(self):
self.invoices = frappe._dict() self.invoices = frappe._dict()
conditions = self.get_conditions() conditions = self.get_conditions()
company_gstins = get_company_gstin_number(self.filters.get('company'), all_gstins=True)
self.filters.update({
'company_gstins': company_gstins
})
invoice_data = frappe.db.sql(""" invoice_data = frappe.db.sql("""
select select
{select_columns} {select_columns}
@ -193,6 +200,9 @@ class Gstr1Report(object):
elif self.filters.get("type_of_business") == "EXPORT": elif self.filters.get("type_of_business") == "EXPORT":
conditions += """ AND is_return !=1 and gst_category = 'Overseas' """ conditions += """ AND is_return !=1 and gst_category = 'Overseas' """
conditions += " AND billing_address_gstin NOT IN %(company_gstins)s"
return conditions return conditions
def get_invoice_items(self): def get_invoice_items(self):
@ -810,7 +820,8 @@ def get_rate_and_tax_details(row, gstin):
return {"num": int(num), "itm_det": itm_det} return {"num": int(num), "itm_det": itm_det}
def get_company_gstin_number(company, address=None): def get_company_gstin_number(company, address=None, all_gstins=False):
gstin = ''
if address: if address:
gstin = frappe.db.get_value("Address", address, "gstin") gstin = frappe.db.get_value("Address", address, "gstin")
@ -822,8 +833,8 @@ def get_company_gstin_number(company, address=None):
["Dynamic Link", "parenttype", "=", "Address"], ["Dynamic Link", "parenttype", "=", "Address"],
] ]
gstin = frappe.get_all("Address", filters=filters, pluck="gstin") gstin = frappe.get_all("Address", filters=filters, pluck="gstin")
if gstin: if gstin and not all_gstins:
gstin[0] gstin = gstin[0]
if not gstin: if not gstin:
address = frappe.bold(address) if address else "" address = frappe.bold(address) if address else ""

View File

@ -8,39 +8,52 @@ from frappe.utils import cint
from erpnext.accounts.doctype.pos_profile.pos_profile import get_item_groups from erpnext.accounts.doctype.pos_profile.pos_profile import get_item_groups
from erpnext.accounts.doctype.pos_invoice.pos_invoice import get_stock_availability from erpnext.accounts.doctype.pos_invoice.pos_invoice import get_stock_availability
from six import string_types def search_by_term(search_term, warehouse, price_list):
result = search_for_serial_or_batch_or_barcode_number(search_term)
item_code = result.get("item_code") or search_term
serial_no = result.get("serial_no") or ""
batch_no = result.get("batch_no") or ""
barcode = result.get("barcode") or ""
if result:
item_info = frappe.db.get_value("Item", item_code,
["name as item_code", "item_name", "description", "stock_uom", "image as item_image", "is_stock_item"],
as_dict=1)
item_stock_qty = get_stock_availability(item_code, warehouse)
price_list_rate, currency = frappe.db.get_value('Item Price', {
'price_list': price_list,
'item_code': item_code
}, ["price_list_rate", "currency"])
item_info.update({
'serial_no': serial_no,
'batch_no': batch_no,
'barcode': barcode,
'price_list_rate': price_list_rate,
'currency': currency,
'actual_qty': item_stock_qty
})
return {'items': [item_info]}
@frappe.whitelist() @frappe.whitelist()
def get_items(start, page_length, price_list, item_group, pos_profile, search_value=""): def get_items(start, page_length, price_list, item_group, pos_profile, search_term=""):
data = dict() warehouse, hide_unavailable_items = frappe.db.get_value(
'POS Profile', pos_profile, ['warehouse', 'hide_unavailable_items'])
result = [] result = []
allow_negative_stock = frappe.db.get_single_value('Stock Settings', 'allow_negative_stock') if search_term:
warehouse, hide_unavailable_items = frappe.db.get_value('POS Profile', pos_profile, ['warehouse', 'hide_unavailable_items']) result = search_by_term(search_term, warehouse, price_list)
if result:
return result
if not frappe.db.exists('Item Group', item_group): if not frappe.db.exists('Item Group', item_group):
item_group = get_root_of('Item Group') item_group = get_root_of('Item Group')
if search_value: condition = get_conditions(search_term)
data = search_serial_or_batch_or_barcode_number(search_value)
item_code = data.get("item_code") if data.get("item_code") else search_value
serial_no = data.get("serial_no") if data.get("serial_no") else ""
batch_no = data.get("batch_no") if data.get("batch_no") else ""
barcode = data.get("barcode") if data.get("barcode") else ""
if data:
item_info = frappe.db.get_value(
"Item", data.get("item_code"),
["name as item_code", "item_name", "description", "stock_uom", "image as item_image", "is_stock_item"]
, as_dict=1)
item_info.setdefault('serial_no', serial_no)
item_info.setdefault('batch_no', batch_no)
item_info.setdefault('barcode', barcode)
return { 'items': [item_info] }
condition = get_conditions(item_code, serial_no, batch_no, barcode)
condition += get_item_group_condition(pos_profile) condition += get_item_group_condition(pos_profile)
lft, rgt = frappe.db.get_value('Item Group', item_group, ['lft', 'rgt']) lft, rgt = frappe.db.get_value('Item Group', item_group, ['lft', 'rgt'])
@ -62,7 +75,6 @@ def get_items(start, page_length, price_list, item_group, pos_profile, search_va
`tabItem` item {bin_join_selection} `tabItem` item {bin_join_selection}
WHERE WHERE
item.disabled = 0 item.disabled = 0
AND item.is_stock_item = 1
AND item.has_variants = 0 AND item.has_variants = 0
AND item.is_sales_item = 1 AND item.is_sales_item = 1
AND item.is_fixed_asset = 0 AND item.is_fixed_asset = 0
@ -84,6 +96,7 @@ def get_items(start, page_length, price_list, item_group, pos_profile, search_va
), {'warehouse': warehouse}, as_dict=1) ), {'warehouse': warehouse}, as_dict=1)
if items_data: if items_data:
items_data = filter_service_items(items_data)
items = [d.item_code for d in items_data] items = [d.item_code for d in items_data]
item_prices_data = frappe.get_all("Item Price", item_prices_data = frappe.get_all("Item Price",
fields = ["item_code", "price_list_rate", "currency"], fields = ["item_code", "price_list_rate", "currency"],
@ -96,9 +109,6 @@ def get_items(start, page_length, price_list, item_group, pos_profile, search_va
for item in items_data: for item in items_data:
item_code = item.item_code item_code = item.item_code
item_price = item_prices.get(item_code) or {} item_price = item_prices.get(item_code) or {}
if allow_negative_stock:
item_stock_qty = frappe.db.sql("""select ifnull(sum(actual_qty), 0) from `tabBin` where item_code = %s""", item_code)[0][0]
else:
item_stock_qty = get_stock_availability(item_code, warehouse) item_stock_qty = get_stock_availability(item_code, warehouse)
row = {} row = {}
@ -110,14 +120,10 @@ def get_items(start, page_length, price_list, item_group, pos_profile, search_va
}) })
result.append(row) result.append(row)
res = { return {'items': result}
'items': result
}
return res
@frappe.whitelist() @frappe.whitelist()
def search_serial_or_batch_or_barcode_number(search_value): def search_for_serial_or_batch_or_barcode_number(search_value):
# search barcode no # search barcode no
barcode_data = frappe.db.get_value('Item Barcode', {'barcode': search_value}, ['barcode', 'parent as item_code'], as_dict=True) barcode_data = frappe.db.get_value('Item Barcode', {'barcode': search_value}, ['barcode', 'parent as item_code'], as_dict=True)
if barcode_data: if barcode_data:
@ -135,27 +141,29 @@ def search_serial_or_batch_or_barcode_number(search_value):
return {} return {}
def get_conditions(item_code, serial_no, batch_no, barcode): def filter_service_items(items):
if serial_no or batch_no or barcode: for item in items:
return "item.name = {0}".format(frappe.db.escape(item_code)) if not item['is_stock_item']:
if not frappe.db.exists('Product Bundle', item['item_code']):
items.remove(item)
return make_condition(item_code) return items
def make_condition(item_code): def get_conditions(search_term):
condition = "(" condition = "("
condition += """item.name like {item_code} condition += """item.name like {search_term}
or item.item_name like {item_code}""".format(item_code = frappe.db.escape('%' + item_code + '%')) or item.item_name like {search_term}""".format(search_term=frappe.db.escape('%' + search_term + '%'))
condition += add_search_fields_condition(item_code) condition += add_search_fields_condition(search_term)
condition += ")" condition += ")"
return condition return condition
def add_search_fields_condition(item_code): def add_search_fields_condition(search_term):
condition = '' condition = ''
search_fields = frappe.get_all('POS Search Fields', fields = ['fieldname']) search_fields = frappe.get_all('POS Search Fields', fields = ['fieldname'])
if search_fields: if search_fields:
for field in search_fields: for field in search_fields:
condition += " or item.{0} like {1}".format(field['fieldname'], frappe.db.escape('%' + item_code + '%')) condition += " or item.`{0}` like {1}".format(field['fieldname'], frappe.db.escape('%' + search_term + '%'))
return condition return condition
def get_item_group_condition(pos_profile): def get_item_group_condition(pos_profile):

View File

@ -241,10 +241,8 @@ erpnext.PointOfSale.Controller = class {
events: { events: {
get_frm: () => this.frm, get_frm: () => this.frm,
cart_item_clicked: (item_code, batch_no, uom) => { cart_item_clicked: (item_code, batch_no, uom, rate) => {
const search_field = batch_no ? 'batch_no' : 'item_code'; const item_row = this.get_item_from_frm(item_code, batch_no, uom, rate);
const search_value = batch_no || item_code;
const item_row = this.frm.doc.items.find(i => i[search_field] === search_value && i.uom === uom);
this.item_details.toggle_item_details_section(item_row); this.item_details.toggle_item_details_section(item_row);
}, },
@ -275,18 +273,25 @@ erpnext.PointOfSale.Controller = class {
this.cart.toggle_numpad(minimize); this.cart.toggle_numpad(minimize);
}, },
form_updated: async (cdt, cdn, fieldname, value) => { form_updated: (cdt, cdn, fieldname, value) => {
const item_row = frappe.model.get_doc(cdt, cdn); const item_row = frappe.model.get_doc(cdt, cdn);
if (item_row && item_row[fieldname] != value) { if (item_row && item_row[fieldname] != value) {
const { item_code, batch_no, uom } = this.item_details.current_item; const { item_code, batch_no, uom, rate } = this.item_details.current_item;
const event = { const event = {
field: fieldname, field: fieldname,
value, value,
item: { item_code, batch_no, uom } item: { item_code, batch_no, uom, rate }
} }
return this.on_cart_update(event) return this.on_cart_update(event)
} }
return Promise.resolve();
},
highlight_cart_item: (item) => {
const cart_item = this.cart.get_cart_item(item);
this.cart.toggle_item_highlight(cart_item);
}, },
item_field_focused: (fieldname) => { item_field_focused: (fieldname) => {
@ -501,8 +506,8 @@ erpnext.PointOfSale.Controller = class {
let item_row = undefined; let item_row = undefined;
try { try {
let { field, value, item } = args; let { field, value, item } = args;
const { item_code, batch_no, serial_no, uom } = item; const { item_code, batch_no, serial_no, uom, rate } = item;
item_row = this.get_item_from_frm(item_code, batch_no, uom); item_row = this.get_item_from_frm(item_code, batch_no, uom, rate);
const item_selected_from_selector = field === 'qty' && value === "+1" const item_selected_from_selector = field === 'qty' && value === "+1"
@ -535,7 +540,7 @@ erpnext.PointOfSale.Controller = class {
item_selected_from_selector && (value = flt(value)) item_selected_from_selector && (value = flt(value))
const args = { item_code, batch_no, [field]: value }; const args = { item_code, batch_no, rate, [field]: value };
if (serial_no) { if (serial_no) {
await this.check_serial_no_availablilty(item_code, this.frm.doc.set_warehouse, serial_no); await this.check_serial_no_availablilty(item_code, this.frm.doc.set_warehouse, serial_no);
@ -551,8 +556,10 @@ erpnext.PointOfSale.Controller = class {
await this.trigger_new_item_events(item_row); await this.trigger_new_item_events(item_row);
this.check_serial_batch_selection_needed(item_row) && this.edit_item_details_of(item_row);
this.update_cart_html(item_row); this.update_cart_html(item_row);
this.item_details.$component.is(':visible') && this.edit_item_details_of(item_row);
this.check_serial_batch_selection_needed(item_row) && this.edit_item_details_of(item_row);
} }
} catch (error) { } catch (error) {
@ -563,12 +570,13 @@ erpnext.PointOfSale.Controller = class {
} }
} }
get_item_from_frm(item_code, batch_no, uom) { get_item_from_frm(item_code, batch_no, uom, rate) {
const has_batch_no = batch_no; const has_batch_no = batch_no;
return this.frm.doc.items.find( return this.frm.doc.items.find(
i => i.item_code === item_code i => i.item_code === item_code
&& (!has_batch_no || (has_batch_no && i.batch_no === batch_no)) && (!has_batch_no || (has_batch_no && i.batch_no === batch_no))
&& (i.uom === uom) && (i.uom === uom)
&& (i.rate == rate)
); );
} }

View File

@ -184,7 +184,8 @@ erpnext.PointOfSale.ItemCart = class {
const item_code = unescape($cart_item.attr('data-item-code')); const item_code = unescape($cart_item.attr('data-item-code'));
const batch_no = unescape($cart_item.attr('data-batch-no')); const batch_no = unescape($cart_item.attr('data-batch-no'));
const uom = unescape($cart_item.attr('data-uom')); const uom = unescape($cart_item.attr('data-uom'));
me.events.cart_item_clicked(item_code, batch_no, uom); const rate = unescape($cart_item.attr('data-rate'));
me.events.cart_item_clicked(item_code, batch_no, uom, rate);
this.numpad_value = ''; this.numpad_value = '';
}); });
@ -520,28 +521,34 @@ erpnext.PointOfSale.ItemCart = class {
} }
} }
get_cart_item({ item_code, batch_no, uom }) { get_cart_item({ item_code, batch_no, uom, rate }) {
const batch_attr = `[data-batch-no="${escape(batch_no)}"]`; const batch_attr = `[data-batch-no="${escape(batch_no)}"]`;
const item_code_attr = `[data-item-code="${escape(item_code)}"]`; const item_code_attr = `[data-item-code="${escape(item_code)}"]`;
const uom_attr = `[data-uom="${escape(uom)}"]`; const uom_attr = `[data-uom="${escape(uom)}"]`;
const rate_attr = `[data-rate="${escape(rate)}"]`;
const item_selector = batch_no ? const item_selector = batch_no ?
`.cart-item-wrapper${batch_attr}${uom_attr}` : `.cart-item-wrapper${item_code_attr}${uom_attr}`; `.cart-item-wrapper${batch_attr}${uom_attr}${rate_attr}` : `.cart-item-wrapper${item_code_attr}${uom_attr}${rate_attr}`;
return this.$cart_items_wrapper.find(item_selector); return this.$cart_items_wrapper.find(item_selector);
} }
get_item_from_frm(item) {
const doc = this.events.get_frm().doc;
const { item_code, batch_no, uom, rate } = item;
const search_field = batch_no ? 'batch_no' : 'item_code';
const search_value = batch_no || item_code;
return doc.items.find(i => i[search_field] === search_value && i.uom === uom && i.rate === rate);
}
update_item_html(item, remove_item) { update_item_html(item, remove_item) {
const $item = this.get_cart_item(item); const $item = this.get_cart_item(item);
if (remove_item) { if (remove_item) {
$item && $item.next().remove() && $item.remove(); $item && $item.next().remove() && $item.remove();
} else { } else {
const { item_code, batch_no, uom } = item; const item_row = this.get_item_from_frm(item);
const search_field = batch_no ? 'batch_no' : 'item_code';
const search_value = batch_no || item_code;
const item_row = this.events.get_frm().doc.items.find(i => i[search_field] === search_value && i.uom === uom);
this.render_cart_item(item_row, $item); this.render_cart_item(item_row, $item);
} }
@ -559,7 +566,7 @@ erpnext.PointOfSale.ItemCart = class {
this.$cart_items_wrapper.append( this.$cart_items_wrapper.append(
`<div class="cart-item-wrapper" `<div class="cart-item-wrapper"
data-item-code="${escape(item_data.item_code)}" data-uom="${escape(item_data.uom)}" data-item-code="${escape(item_data.item_code)}" data-uom="${escape(item_data.uom)}"
data-batch-no="${escape(item_data.batch_no || '')}"> data-batch-no="${escape(item_data.batch_no || '')}" data-rate="${escape(item_data.rate)}">
</div> </div>
<div class="seperator"></div>` <div class="seperator"></div>`
) )
@ -636,13 +643,23 @@ erpnext.PointOfSale.ItemCart = class {
function get_item_image_html() { function get_item_image_html() {
const { image, item_name } = item_data; const { image, item_name } = item_data;
if (image) { if (image) {
return `<div class="item-image"><img src="${image}" alt="${image}""></div>`; return `
<div class="item-image">
<img
onerror="cur_pos.cart.handle_broken_image(this)"
src="${image}" alt="${frappe.get_abbr(item_name)}"">
</div>`;
} else { } else {
return `<div class="item-image item-abbr">${frappe.get_abbr(item_name)}</div>`; return `<div class="item-image item-abbr">${frappe.get_abbr(item_name)}</div>`;
} }
} }
} }
handle_broken_image($img) {
const item_abbr = $($img).attr('alt');
$($img).parent().replaceWith(`<div class="item-image item-abbr">${item_abbr}</div>`);
}
scroll_to_item($item) { scroll_to_item($item) {
if ($item.length === 0) return; if ($item.length === 0) return;
const scrollTop = $item.offset().top - this.$cart_items_wrapper.offset().top + this.$cart_items_wrapper.scrollTop(); const scrollTop = $item.offset().top - this.$cart_items_wrapper.offset().top + this.$cart_items_wrapper.scrollTop();

View File

@ -54,13 +54,24 @@ erpnext.PointOfSale.ItemDetails = class {
this.$dicount_section = this.$component.find('.discount-section'); this.$dicount_section = this.$component.find('.discount-section');
} }
toggle_item_details_section(item) { has_item_has_changed(item) {
const { item_code, batch_no, uom } = this.current_item; const { item_code, batch_no, uom, rate } = this.current_item;
const item_code_is_same = item && item_code === item.item_code; const item_code_is_same = item && item_code === item.item_code;
const batch_is_same = item && batch_no == item.batch_no; const batch_is_same = item && batch_no == item.batch_no;
const uom_is_same = item && uom === item.uom; const uom_is_same = item && uom === item.uom;
const rate_is_same = item && rate === item.rate;
this.item_has_changed = !item ? false : item_code_is_same && batch_is_same && uom_is_same ? false : true; if (!item)
return false;
if (item_code_is_same && batch_is_same && uom_is_same && rate_is_same)
return false;
return true;
}
toggle_item_details_section(item) {
this.item_has_changed = this.has_item_has_changed(item);
this.events.toggle_item_selector(this.item_has_changed); this.events.toggle_item_selector(this.item_has_changed);
this.toggle_component(this.item_has_changed); this.toggle_component(this.item_has_changed);
@ -72,11 +83,12 @@ erpnext.PointOfSale.ItemDetails = class {
this.item_row = item; this.item_row = item;
this.currency = this.events.get_frm().doc.currency; this.currency = this.events.get_frm().doc.currency;
this.current_item = { item_code: item.item_code, batch_no: item.batch_no, uom: item.uom }; this.current_item = { item_code: item.item_code, batch_no: item.batch_no, uom: item.uom, rate: item.rate };
this.render_dom(item); this.render_dom(item);
this.render_discount_dom(item); this.render_discount_dom(item);
this.render_form(item); this.render_form(item);
this.events.highlight_cart_item(item);
} else { } else {
this.validate_serial_batch_item(); this.validate_serial_batch_item();
this.current_item = {}; this.current_item = {};
@ -121,13 +133,24 @@ erpnext.PointOfSale.ItemDetails = class {
this.$item_description.html(get_description_html()); this.$item_description.html(get_description_html());
this.$item_price.html(format_currency(price_list_rate, this.currency)); this.$item_price.html(format_currency(price_list_rate, this.currency));
if (image) { if (image) {
this.$item_image.html(`<img src="${image}" alt="${image}">`); this.$item_image.html(
`<img
onerror="cur_pos.item_details.handle_broken_image(this)"
class="h-full" src="${image}"
alt="${frappe.get_abbr(item_name)}"
style="object-fit: cover;">`
);
} else { } else {
this.$item_image.html(`<div class="item-abbr">${frappe.get_abbr(item_name)}</div>`); this.$item_image.html(`<div class="item-abbr">${frappe.get_abbr(item_name)}</div>`);
} }
} }
handle_broken_image($img) {
const item_abbr = $($img).attr('alt');
$($img).replaceWith(`<div class="item-abbr">${item_abbr}</div>`);
}
render_discount_dom(item) { render_discount_dom(item) {
if (item.discount_percentage) { if (item.discount_percentage) {
this.$dicount_section.html( this.$dicount_section.html(
@ -198,12 +221,14 @@ erpnext.PointOfSale.ItemDetails = class {
if (this.allow_rate_change) { if (this.allow_rate_change) {
this.rate_control.df.onchange = function() { this.rate_control.df.onchange = function() {
if (this.value || flt(this.value) === 0) { if (this.value || flt(this.value) === 0) {
me.events.set_value_in_current_cart_item('rate', this.value);
me.events.form_updated(me.doctype, me.name, 'rate', this.value).then(() => { me.events.form_updated(me.doctype, me.name, 'rate', this.value).then(() => {
const item_row = frappe.get_doc(me.doctype, me.name); const item_row = frappe.get_doc(me.doctype, me.name);
const doc = me.events.get_frm().doc; const doc = me.events.get_frm().doc;
me.$item_price.html(format_currency(item_row.rate, doc.currency)); me.$item_price.html(format_currency(item_row.rate, doc.currency));
me.render_discount_dom(item_row); me.render_discount_dom(item_row);
}); });
me.current_item.rate = this.value;
} }
}; };
} else { } else {
@ -292,11 +317,7 @@ erpnext.PointOfSale.ItemDetails = class {
frappe.model.on("POS Invoice Item", "*", (fieldname, value, item_row) => { frappe.model.on("POS Invoice Item", "*", (fieldname, value, item_row) => {
const field_control = this[`${fieldname}_control`]; const field_control = this[`${fieldname}_control`];
const { item_code, batch_no, uom } = this.current_item; const item_is_same = !this.has_item_has_changed(item_row);
const item_code_is_same = item_code === item_row.item_code;
const batch_is_same = batch_no == item_row.batch_no;
const uom_is_same = uom === item_row.uom;
const item_is_same = item_code_is_same && batch_is_same && uom_is_same ? true : false;
if (item_is_same && field_control && field_control.get_value() !== value) { if (item_is_same && field_control && field_control.get_value() !== value) {
field_control.set_value(value); field_control.set_value(value);

View File

@ -51,7 +51,7 @@ erpnext.PointOfSale.ItemSelector = class {
}); });
} }
get_items({start = 0, page_length = 40, search_value=''}) { get_items({start = 0, page_length = 40, search_term=''}) {
const doc = this.events.get_frm().doc; const doc = this.events.get_frm().doc;
const price_list = (doc && doc.selling_price_list) || this.price_list; const price_list = (doc && doc.selling_price_list) || this.price_list;
let { item_group, pos_profile } = this; let { item_group, pos_profile } = this;
@ -61,7 +61,7 @@ erpnext.PointOfSale.ItemSelector = class {
return frappe.call({ return frappe.call({
method: "erpnext.selling.page.point_of_sale.point_of_sale.get_items", method: "erpnext.selling.page.point_of_sale.point_of_sale.get_items",
freeze: true, freeze: true,
args: { start, page_length, price_list, item_group, search_value, pos_profile }, args: { start, page_length, price_list, item_group, search_term, pos_profile },
}); });
} }
@ -78,8 +78,9 @@ erpnext.PointOfSale.ItemSelector = class {
get_item_html(item) { get_item_html(item) {
const me = this; const me = this;
// eslint-disable-next-line no-unused-vars // eslint-disable-next-line no-unused-vars
const { item_image, serial_no, batch_no, barcode, actual_qty, stock_uom } = item; const { item_image, serial_no, batch_no, barcode, actual_qty, stock_uom, price_list_rate } = item;
const indicator_color = actual_qty > 10 ? "green" : actual_qty <= 0 ? "red" : "orange"; const indicator_color = actual_qty > 10 ? "green" : actual_qty <= 0 ? "red" : "orange";
const precision = flt(price_list_rate, 2) % 1 != 0 ? 2 : 0;
let qty_to_display = actual_qty; let qty_to_display = actual_qty;
@ -94,7 +95,11 @@ erpnext.PointOfSale.ItemSelector = class {
<span class="indicator-pill whitespace-nowrap ${indicator_color}">${qty_to_display}</span> <span class="indicator-pill whitespace-nowrap ${indicator_color}">${qty_to_display}</span>
</div> </div>
<div class="flex items-center justify-center h-32 border-b-grey text-6xl text-grey-100"> <div class="flex items-center justify-center h-32 border-b-grey text-6xl text-grey-100">
<img class="h-full" src="${item_image}" alt="${frappe.get_abbr(item.item_name)}" style="object-fit: cover;"> <img
onerror="cur_pos.item_selector.handle_broken_image(this)"
class="h-full" src="${item_image}"
alt="${frappe.get_abbr(item.item_name)}"
style="object-fit: cover;">
</div>`; </div>`;
} else { } else {
return `<div class="item-qty-pill"> return `<div class="item-qty-pill">
@ -108,6 +113,7 @@ erpnext.PointOfSale.ItemSelector = class {
`<div class="item-wrapper" `<div class="item-wrapper"
data-item-code="${escape(item.item_code)}" data-serial-no="${escape(serial_no)}" data-item-code="${escape(item.item_code)}" data-serial-no="${escape(serial_no)}"
data-batch-no="${escape(batch_no)}" data-uom="${escape(stock_uom)}" data-batch-no="${escape(batch_no)}" data-uom="${escape(stock_uom)}"
data-rate="${escape(price_list_rate)}"
title="${item.item_name}"> title="${item.item_name}">
${get_item_image_html()} ${get_item_image_html()}
@ -116,12 +122,17 @@ erpnext.PointOfSale.ItemSelector = class {
<div class="item-name"> <div class="item-name">
${frappe.ellipsis(item.item_name, 18)} ${frappe.ellipsis(item.item_name, 18)}
</div> </div>
<div class="item-rate">${format_currency(item.price_list_rate, item.currency, 0) || 0}</div> <div class="item-rate">${format_currency(price_list_rate, item.currency, precision) || 0}</div>
</div> </div>
</div>` </div>`
); );
} }
handle_broken_image($img) {
const item_abbr = $($img).attr('alt');
$($img).parent().replaceWith(`<div class="item-display abbr">${item_abbr}</div>`);
}
make_search_bar() { make_search_bar() {
const me = this; const me = this;
const doc = me.events.get_frm().doc; const doc = me.events.get_frm().doc;
@ -213,13 +224,15 @@ erpnext.PointOfSale.ItemSelector = class {
let batch_no = unescape($item.attr('data-batch-no')); let batch_no = unescape($item.attr('data-batch-no'));
let serial_no = unescape($item.attr('data-serial-no')); let serial_no = unescape($item.attr('data-serial-no'));
let uom = unescape($item.attr('data-uom')); let uom = unescape($item.attr('data-uom'));
let rate = unescape($item.attr('data-rate'));
// escape(undefined) returns "undefined" then unescape returns "undefined" // escape(undefined) returns "undefined" then unescape returns "undefined"
batch_no = batch_no === "undefined" ? undefined : batch_no; batch_no = batch_no === "undefined" ? undefined : batch_no;
serial_no = serial_no === "undefined" ? undefined : serial_no; serial_no = serial_no === "undefined" ? undefined : serial_no;
uom = uom === "undefined" ? undefined : uom; uom = uom === "undefined" ? undefined : uom;
rate = rate === "undefined" ? undefined : rate;
me.events.item_selected({ field: 'qty', value: "+1", item: { item_code, batch_no, serial_no, uom }}); me.events.item_selected({ field: 'qty', value: "+1", item: { item_code, batch_no, serial_no, uom, rate }});
me.set_search_value(''); me.set_search_value('');
}); });
@ -290,7 +303,7 @@ erpnext.PointOfSale.ItemSelector = class {
} }
} }
this.get_items({ search_value: search_term }) this.get_items({ search_term })
.then(({ message }) => { .then(({ message }) => {
// eslint-disable-next-line no-unused-vars // eslint-disable-next-line no-unused-vars
const { items, serial_no, batch_no, barcode } = message; const { items, serial_no, batch_no, barcode } = message;

View File

@ -171,7 +171,7 @@ erpnext.PointOfSale.Payment = class {
this.setup_listener_for_payments(); this.setup_listener_for_payments();
this.$payment_modes.on('click', '.shortcut', () => { this.$payment_modes.on('click', '.shortcut', function() {
const value = $(this).attr('data-value'); const value = $(this).attr('data-value');
me.selected_mode.set_value(value); me.selected_mode.set_value(value);
}); });

View File

@ -249,7 +249,7 @@ class EmailDigest(Document):
card = cache.get(cache_key) card = cache.get(cache_key)
if card: if card:
card = eval(card) card = frappe.safe_eval(card)
else: else:
card = frappe._dict(getattr(self, "get_" + key)()) card = frappe._dict(getattr(self, "get_" + key)())
@ -808,7 +808,6 @@ def get_incomes_expenses_for_period(account, from_date, to_date):
val = balance_on_to_date - balance_before_from_date val = balance_on_to_date - balance_before_from_date
else: else:
last_year_closing_balance = get_balance_on(account, date=fy_start_date - timedelta(days=1)) last_year_closing_balance = get_balance_on(account, date=fy_start_date - timedelta(days=1))
print(fy_start_date - timedelta(days=1), last_year_closing_balance)
val = balance_on_to_date + (last_year_closing_balance - balance_before_from_date) val = balance_on_to_date + (last_year_closing_balance - balance_before_from_date)
return val return val

View File

@ -12,10 +12,6 @@ from frappe.desk.notifications import clear_notifications
class TransactionDeletionRecord(Document): class TransactionDeletionRecord(Document):
def validate(self): def validate(self):
frappe.only_for('System Manager') frappe.only_for('System Manager')
company_obj = frappe.get_doc('Company', self.company)
if frappe.session.user != company_obj.owner and frappe.session.user != 'Administrator':
frappe.throw(_('Transactions can only be deleted by the creator of the Company or the Administrator.'),
frappe.PermissionError)
doctypes_to_be_ignored_list = get_doctypes_to_be_ignored() doctypes_to_be_ignored_list = get_doctypes_to_be_ignored()
for doctype in self.doctypes_to_be_ignored: for doctype in self.doctypes_to_be_ignored:
if doctype.doctype_name not in doctypes_to_be_ignored_list: if doctype.doctype_name not in doctypes_to_be_ignored_list:

View File

@ -481,37 +481,42 @@
}, },
"Germany": { "Germany": {
"tax_categories": [
"Umsatzsteuer",
"Vorsteuer"
],
"chart_of_accounts": { "chart_of_accounts": {
"SKR04 mit Kontonummern": { "SKR04 mit Kontonummern": {
"sales_tax_templates": [ "sales_tax_templates": [
{ {
"title": "Umsatzsteuer 19%", "title": "Umsatzsteuer",
"tax_category": "Umsatzsteuer",
"is_default": 1,
"taxes": [ "taxes": [
{ {
"account_head": { "account_head": {
"account_name": "Umsatzsteuer 19%", "account_name": "Umsatzsteuer 19%",
"account_number": "3806", "account_number": "3806",
"tax_rate": 19.00 "tax_rate": 19.00
}
}
]
}, },
{ "rate": 0.00
"title": "Umsatzsteuer 7%", },
"taxes": [
{ {
"account_head": { "account_head": {
"account_name": "Umsatzsteuer 7%", "account_name": "Umsatzsteuer 7%",
"account_number": "3801", "account_number": "3801",
"tax_rate": 7.00 "tax_rate": 7.00
} },
"rate": 0.00
} }
] ]
} }
], ],
"purchase_tax_templates": [ "purchase_tax_templates": [
{ {
"title": "Abziehbare Vorsteuer 19%", "title": "Vorsteuer",
"tax_category": "Vorsteuer",
"is_default": 1,
"taxes": [ "taxes": [
{ {
"account_head": { "account_head": {
@ -519,20 +524,17 @@
"account_number": "1406", "account_number": "1406",
"root_type": "Asset", "root_type": "Asset",
"tax_rate": 19.00 "tax_rate": 19.00
}
}
]
}, },
{ "rate": 0.00
"title": "Abziehbare Vorsteuer 7%", },
"taxes": [
{ {
"account_head": { "account_head": {
"account_name": "Abziehbare Vorsteuer 7%", "account_name": "Abziehbare Vorsteuer 7%",
"account_number": "1401", "account_number": "1401",
"root_type": "Asset", "root_type": "Asset",
"tax_rate": 7.00 "tax_rate": 7.00
} },
"rate": 0.00
} }
] ]
}, },
@ -559,38 +561,129 @@
} }
] ]
} }
] ],
}, "item_tax_templates": [
"SKR03 mit Kontonummern": {
"sales_tax_templates": [
{ {
"title": "Umsatzsteuer 19%", "title": "Umsatzsteuer 19%",
"taxes": [ "taxes": [
{ {
"account_head": { "tax_type": {
"account_name": "Umsatzsteuer 19%", "account_name": "Umsatzsteuer 19%",
"account_number": "1776", "account_number": "3806",
"tax_rate": 19.00 "tax_rate": 19.00
} },
"tax_rate": 19.00
},
{
"tax_type": {
"account_name": "Umsatzsteuer 7%",
"account_number": "3801",
"tax_rate": 7.00
},
"tax_rate": 0.00
} }
] ]
}, },
{ {
"title": "Umsatzsteuer 7%", "title": "Umsatzsteuer 7%",
"taxes": [ "taxes": [
{
"tax_type": {
"account_name": "Umsatzsteuer 19%",
"account_number": "3806",
"tax_rate": 19.00
},
"tax_rate": 0.00
},
{
"tax_type": {
"account_name": "Umsatzsteuer 7%",
"account_number": "3801",
"tax_rate": 7.00
},
"tax_rate": 7.00
}
]
},
{
"title": "Vorsteuer 19%",
"taxes": [
{
"tax_type": {
"account_name": "Abziehbare Vorsteuer 19%",
"account_number": "1406",
"root_type": "Asset",
"tax_rate": 19.00
},
"tax_rate": 19.00
},
{
"tax_type": {
"account_name": "Abziehbare Vorsteuer 7%",
"account_number": "1401",
"root_type": "Asset",
"tax_rate": 7.00
},
"tax_rate": 0.00
}
]
},
{
"title": "Vorsteuer 7%",
"taxes": [
{
"tax_type": {
"account_name": "Abziehbare Vorsteuer 19%",
"account_number": "1406",
"root_type": "Asset",
"tax_rate": 19.00
},
"tax_rate": 0.00
},
{
"tax_type": {
"account_name": "Abziehbare Vorsteuer 7%",
"account_number": "1401",
"root_type": "Asset",
"tax_rate": 7.00
},
"tax_rate": 7.00
}
]
}
]
},
"SKR03 mit Kontonummern": {
"sales_tax_templates": [
{
"title": "Umsatzsteuer",
"tax_category": "Umsatzsteuer",
"is_default": 1,
"taxes": [
{
"account_head": {
"account_name": "Umsatzsteuer 19%",
"account_number": "1776",
"tax_rate": 19.00
},
"rate": 0.00
},
{ {
"account_head": { "account_head": {
"account_name": "Umsatzsteuer 7%", "account_name": "Umsatzsteuer 7%",
"account_number": "1771", "account_number": "1771",
"tax_rate": 7.00 "tax_rate": 7.00
} },
"rate": 0.00
} }
] ]
} }
], ],
"purchase_tax_templates": [ "purchase_tax_templates": [
{ {
"title": "Abziehbare Vorsteuer 19%", "title": "Vorsteuer",
"tax_category": "Vorsteuer",
"is_default": 1,
"taxes": [ "taxes": [
{ {
"account_head": { "account_head": {
@ -598,20 +691,107 @@
"account_number": "1576", "account_number": "1576",
"root_type": "Asset", "root_type": "Asset",
"tax_rate": 19.00 "tax_rate": 19.00
}
}
]
}, },
{ "rate": 0.00
"title": "Abziehbare Vorsteuer 7%", },
"taxes": [
{ {
"account_head": { "account_head": {
"account_name": "Abziehbare Vorsteuer 7%", "account_name": "Abziehbare Vorsteuer 7%",
"account_number": "1571", "account_number": "1571",
"root_type": "Asset", "root_type": "Asset",
"tax_rate": 7.00 "tax_rate": 7.00
},
"rate": 0.00
} }
]
}
],
"item_tax_templates": [
{
"title": "Umsatzsteuer 19%",
"taxes": [
{
"tax_type": {
"account_name": "Umsatzsteuer 19%",
"account_number": "1776",
"tax_rate": 19.00
},
"tax_rate": 19.00
},
{
"tax_type": {
"account_name": "Umsatzsteuer 7%",
"account_number": "1771",
"tax_rate": 7.00
},
"tax_rate": 0.00
}
]
},
{
"title": "Umsatzsteuer 7%",
"taxes": [
{
"tax_type": {
"account_name": "Umsatzsteuer 19%",
"account_number": "1776",
"tax_rate": 19.00
},
"tax_rate": 0.00
},
{
"tax_type": {
"account_name": "Umsatzsteuer 7%",
"account_number": "1771",
"tax_rate": 7.00
},
"tax_rate": 7.00
}
]
},
{
"title": "Vorsteuer 19%",
"taxes": [
{
"tax_type": {
"account_name": "Abziehbare Vorsteuer 19%",
"account_number": "1576",
"root_type": "Asset",
"tax_rate": 19.00
},
"tax_rate": 19.00
},
{
"tax_type": {
"account_name": "Abziehbare Vorsteuer 7%",
"account_number": "1571",
"root_type": "Asset",
"tax_rate": 7.00
},
"tax_rate": 0.00
}
]
},
{
"title": "Vorsteuer 7%",
"taxes": [
{
"tax_type": {
"account_name": "Abziehbare Vorsteuer 19%",
"account_number": "1576",
"root_type": "Asset",
"tax_rate": 19.00
},
"tax_rate": 0.00
},
{
"tax_type": {
"account_name": "Abziehbare Vorsteuer 7%",
"account_number": "1571",
"root_type": "Asset",
"tax_rate": 7.00
},
"tax_rate": 7.00
} }
] ]
} }
@ -620,33 +800,34 @@
"Standard with Numbers": { "Standard with Numbers": {
"sales_tax_templates": [ "sales_tax_templates": [
{ {
"title": "Umsatzsteuer 19%", "title": "Umsatzsteuer",
"tax_category": "Umsatzsteuer",
"is_default": 1,
"taxes": [ "taxes": [
{ {
"account_head": { "account_head": {
"account_name": "Umsatzsteuer 19%", "account_name": "Umsatzsteuer 19%",
"account_number": "2301", "account_number": "2301",
"tax_rate": 19.00 "tax_rate": 19.00
}
}
]
}, },
{ "rate": 0.00
"title": "Umsatzsteuer 7%", },
"taxes": [
{ {
"account_head": { "account_head": {
"account_name": "Umsatzsteuer 7%", "account_name": "Umsatzsteuer 7%",
"account_number": "2302", "account_number": "2302",
"tax_rate": 7.00 "tax_rate": 7.00
} },
"rate": 0.00
} }
] ]
} }
], ],
"purchase_tax_templates": [ "purchase_tax_templates": [
{ {
"title": "Abziehbare Vorsteuer 19%", "title": "Vorsteuer",
"tax_category": "Vorsteuer",
"is_default": 1,
"taxes": [ "taxes": [
{ {
"account_head": { "account_head": {
@ -654,20 +835,107 @@
"account_number": "1501", "account_number": "1501",
"root_type": "Asset", "root_type": "Asset",
"tax_rate": 19.00 "tax_rate": 19.00
}
}
]
}, },
{ "rate": 0.00
"title": "Abziehbare Vorsteuer 7%", },
"taxes": [
{ {
"account_head": { "account_head": {
"account_name": "Abziehbare Vorsteuer 7%", "account_name": "Abziehbare Vorsteuer 7%",
"account_number": "1502", "account_number": "1502",
"root_type": "Asset", "root_type": "Asset",
"tax_rate": 7.00 "tax_rate": 7.00
},
"rate": 0.00
} }
]
}
],
"item_tax_templates": [
{
"title": "Umsatzsteuer 19%",
"taxes": [
{
"tax_type": {
"account_name": "Umsatzsteuer 19%",
"account_number": "2301",
"tax_rate": 19.00
},
"tax_rate": 19.00
},
{
"tax_type": {
"account_name": "Umsatzsteuer 7%",
"account_number": "2302",
"tax_rate": 7.00
},
"tax_rate": 0.00
}
]
},
{
"title": "Umsatzsteuer 7%",
"taxes": [
{
"tax_type": {
"account_name": "Umsatzsteuer 19%",
"account_number": "2301",
"tax_rate": 19.00
},
"tax_rate": 0.00
},
{
"tax_type": {
"account_name": "Umsatzsteuer 7%",
"account_number": "2302",
"tax_rate": 7.00
},
"tax_rate": 7.00
}
]
},
{
"title": "Vorsteuer 19%",
"taxes": [
{
"tax_type": {
"account_name": "Abziehbare Vorsteuer 19%",
"account_number": "1501",
"root_type": "Asset",
"tax_rate": 19.00
},
"tax_rate": 19.00
},
{
"tax_type": {
"account_name": "Abziehbare Vorsteuer 7%",
"account_number": "1502",
"root_type": "Asset",
"tax_rate": 7.00
},
"tax_rate": 0.00
}
]
},
{
"title": "Vorsteuer 7%",
"taxes": [
{
"tax_type": {
"account_name": "Abziehbare Vorsteuer 19%",
"account_number": "1501",
"root_type": "Asset",
"tax_rate": 19.00
},
"tax_rate": 0.00
},
{
"tax_type": {
"account_name": "Abziehbare Vorsteuer 7%",
"account_number": "1502",
"root_type": "Asset",
"tax_rate": 7.00
},
"tax_rate": 7.00
} }
] ]
} }
@ -676,13 +944,69 @@
"*": { "*": {
"sales_tax_templates": [ "sales_tax_templates": [
{ {
"title": "Umsatzsteuer 19%", "title": "Umsatzsteuer",
"tax_category": "Umsatzsteuer",
"is_default": 1,
"taxes": [ "taxes": [
{ {
"account_head": { "account_head": {
"account_name": "Umsatzsteuer 19%", "account_name": "Umsatzsteuer 19%",
"tax_rate": 19.00 "tax_rate": 19.00
},
"rate": 0.00
},
{
"account_head": {
"account_name": "Umsatzsteuer 7%",
"tax_rate": 7.00
},
"rate": 0.00
} }
]
}
],
"purchase_tax_templates": [
{
"title": "Vorsteuer 19%",
"tax_category": "Vorsteuer",
"is_default": 1,
"taxes": [
{
"account_head": {
"account_name": "Abziehbare Vorsteuer 19%",
"tax_rate": 19.00,
"root_type": "Asset"
},
"rate": 0.00
},
{
"account_head": {
"account_name": "Abziehbare Vorsteuer 7%",
"root_type": "Asset",
"tax_rate": 7.00
},
"rate": 0.00
}
]
}
],
"item_tax_templates": [
{
"title": "Umsatzsteuer 19%",
"taxes": [
{
"tax_type": {
"account_name": "Umsatzsteuer 19%",
"tax_rate": 19.00
},
"tax_rate": 19.00
},
{
"tax_type": {
"account_name": "Umsatzsteuer 7%",
"tax_rate": 7.00
},
"tax_rate": 0.00
} }
] ]
}, },
@ -690,36 +1014,60 @@
"title": "Umsatzsteuer 7%", "title": "Umsatzsteuer 7%",
"taxes": [ "taxes": [
{ {
"account_head": { "tax_type": {
"account_name": "Umsatzsteuer 19%",
"tax_rate": 19.00
},
"tax_rate": 0.00
},
{
"tax_type": {
"account_name": "Umsatzsteuer 7%", "account_name": "Umsatzsteuer 7%",
"tax_rate": 7.00 "tax_rate": 7.00
} },
} "tax_rate": 7.00
]
}
],
"purchase_tax_templates": [
{
"title": "Abziehbare Vorsteuer 19%",
"taxes": [
{
"account_head": {
"account_name": "Abziehbare Vorsteuer 19%",
"tax_rate": 19.00,
"root_type": "Asset"
}
} }
] ]
}, },
{ {
"title": "Abziehbare Vorsteuer 7%", "title": "Vorsteuer 19%",
"taxes": [ "taxes": [
{ {
"account_head": { "tax_type": {
"account_name": "Abziehbare Vorsteuer 19%",
"root_type": "Asset",
"tax_rate": 19.00
},
"tax_rate": 19.00
},
{
"tax_type": {
"account_name": "Abziehbare Vorsteuer 7%", "account_name": "Abziehbare Vorsteuer 7%",
"root_type": "Asset", "root_type": "Asset",
"tax_rate": 7.00 "tax_rate": 7.00
},
"tax_rate": 0.00
} }
]
},
{
"title": "Vorsteuer 7%",
"taxes": [
{
"tax_type": {
"account_name": "Abziehbare Vorsteuer 19%",
"root_type": "Asset",
"tax_rate": 19.00
},
"tax_rate": 0.00
},
{
"tax_type": {
"account_name": "Abziehbare Vorsteuer 7%",
"root_type": "Asset",
"tax_rate": 7.00
},
"tax_rate": 7.00
} }
] ]
} }

View File

@ -11,6 +11,9 @@ from frappe import _
def setup_taxes_and_charges(company_name: str, country: str): def setup_taxes_and_charges(company_name: str, country: str):
if not frappe.db.exists('Company', company_name):
frappe.throw(_('Company {} does not exist yet. Taxes setup aborted.').format(company_name))
file_path = os.path.join(os.path.dirname(__file__), '..', 'data', 'country_wise_tax.json') file_path = os.path.join(os.path.dirname(__file__), '..', 'data', 'country_wise_tax.json')
with open(file_path, 'r') as json_file: with open(file_path, 'r') as json_file:
tax_data = json.load(json_file) tax_data = json.load(json_file)
@ -23,7 +26,7 @@ def setup_taxes_and_charges(company_name: str, country: str):
if 'chart_of_accounts' not in country_wise_tax: if 'chart_of_accounts' not in country_wise_tax:
country_wise_tax = simple_to_detailed(country_wise_tax) country_wise_tax = simple_to_detailed(country_wise_tax)
from_detailed_data(company_name, country_wise_tax.get('chart_of_accounts')) from_detailed_data(company_name, country_wise_tax)
def simple_to_detailed(templates): def simple_to_detailed(templates):
@ -74,10 +77,16 @@ def simple_to_detailed(templates):
def from_detailed_data(company_name, data): def from_detailed_data(company_name, data):
"""Create Taxes and Charges Templates from detailed data.""" """Create Taxes and Charges Templates from detailed data."""
coa_name = frappe.db.get_value('Company', company_name, 'chart_of_accounts') coa_name = frappe.db.get_value('Company', company_name, 'chart_of_accounts')
tax_templates = data.get(coa_name) or data.get('*') coa_data = data.get('chart_of_accounts', {})
sales_tax_templates = tax_templates.get('sales_tax_templates') or tax_templates.get('*') tax_templates = coa_data.get(coa_name) or coa_data.get('*', {})
purchase_tax_templates = tax_templates.get('purchase_tax_templates') or tax_templates.get('*') tax_categories = data.get('tax_categories')
item_tax_templates = tax_templates.get('item_tax_templates') or tax_templates.get('*') sales_tax_templates = tax_templates.get('sales_tax_templates') or tax_templates.get('*', {})
purchase_tax_templates = tax_templates.get('purchase_tax_templates') or tax_templates.get('*', {})
item_tax_templates = tax_templates.get('item_tax_templates') or tax_templates.get('*', {})
if tax_categories:
for tax_category in tax_categories:
make_tax_catgory(tax_category)
if sales_tax_templates: if sales_tax_templates:
for template in sales_tax_templates: for template in sales_tax_templates:
@ -106,6 +115,9 @@ def make_taxes_and_charges_template(company_name, doctype, template):
'charge_type': 'On Net Total' 'charge_type': 'On Net Total'
} }
if doctype == 'Purchase Taxes and Charges Template':
tax_row_defaults['add_deduct_tax'] = 'Add'
# if account_head is a dict, search or create the account and get it's name # if account_head is a dict, search or create the account and get it's name
if isinstance(account_data, dict): if isinstance(account_data, dict):
tax_row_defaults['description'] = '{0} @ {1}'.format(account_data.get('account_name'), account_data.get('tax_rate')) tax_row_defaults['description'] = '{0} @ {1}'.format(account_data.get('account_name'), account_data.get('tax_rate'))
@ -230,3 +242,14 @@ def get_or_create_tax_group(company_name, root_type):
tax_group_name = tax_group_account.name tax_group_name = tax_group_account.name
return tax_group_name return tax_group_name
def make_tax_catgory(tax_category):
doctype = 'Tax Category'
if isinstance(tax_category, str):
tax_category = {'title': tax_category}
tax_category['doctype'] = doctype
if not frappe.db.exists(doctype, tax_category['title']):
doc = frappe.get_doc(tax_category)
doc.insert(ignore_permissions=True)

View File

@ -7,7 +7,7 @@ import frappe
from frappe.utils import nowdate, add_months from frappe.utils import nowdate, add_months
from erpnext.shopping_cart.cart import _get_cart_quotation, update_cart, get_party from erpnext.shopping_cart.cart import _get_cart_quotation, update_cart, get_party
from erpnext.tests.utils import create_test_contact_and_address from erpnext.tests.utils import create_test_contact_and_address
from erpnext.accounts.doctype.tax_rule.tax_rule import ConflictingTaxRule
# test_dependencies = ['Payment Terms Template'] # test_dependencies = ['Payment Terms Template']
@ -125,7 +125,7 @@ class TestShoppingCart(unittest.TestCase):
tax_rule = frappe.get_test_records("Tax Rule")[0] tax_rule = frappe.get_test_records("Tax Rule")[0]
try: try:
frappe.get_doc(tax_rule).insert() frappe.get_doc(tax_rule).insert()
except frappe.DuplicateEntryError: except (frappe.DuplicateEntryError, ConflictingTaxRule):
pass pass
def create_quotation(self): def create_quotation(self):

View File

@ -1,8 +1,6 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors # Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
# License: GNU General Public License v3. See license.txt # License: GNU General Public License v3. See license.txt
from __future__ import unicode_literals
import itertools import itertools
import json import json
import erpnext import erpnext
@ -12,7 +10,7 @@ from erpnext.controllers.item_variant import (ItemVariantExistsError,
copy_attributes_to_variant, get_variant, make_variant_item_code, validate_item_variant_attributes) copy_attributes_to_variant, get_variant, make_variant_item_code, validate_item_variant_attributes)
from erpnext.setup.doctype.item_group.item_group import (get_parent_item_groups, invalidate_cache_for) from erpnext.setup.doctype.item_group.item_group import (get_parent_item_groups, invalidate_cache_for)
from frappe import _, msgprint from frappe import _, msgprint
from frappe.utils import (cint, cstr, flt, formatdate, get_timestamp, getdate, from frappe.utils import (cint, cstr, flt, formatdate, getdate,
now_datetime, random_string, strip, get_link_to_form, nowtime) now_datetime, random_string, strip, get_link_to_form, nowtime)
from frappe.utils.html_utils import clean_html from frappe.utils.html_utils import clean_html
from frappe.website.doctype.website_slideshow.website_slideshow import \ from frappe.website.doctype.website_slideshow.website_slideshow import \
@ -21,8 +19,6 @@ from frappe.website.doctype.website_slideshow.website_slideshow import \
from frappe.website.render import clear_cache from frappe.website.render import clear_cache
from frappe.website.website_generator import WebsiteGenerator from frappe.website.website_generator import WebsiteGenerator
from six import iteritems
class DuplicateReorderRows(frappe.ValidationError): class DuplicateReorderRows(frappe.ValidationError):
pass pass
@ -76,8 +72,6 @@ class Item(WebsiteGenerator):
if not self.description: if not self.description:
self.description = self.item_name self.description = self.item_name
# if self.is_sales_item and not self.get('is_item_from_hub'):
# self.publish_in_hub = 1
def after_insert(self): def after_insert(self):
'''set opening stock and item price''' '''set opening stock and item price'''
@ -129,7 +123,7 @@ class Item(WebsiteGenerator):
self.cant_change() self.cant_change()
self.update_show_in_website() self.update_show_in_website()
if not self.get("__islocal"): 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")
self.old_website_item_groups = frappe.db.sql_list("""select item_group self.old_website_item_groups = frappe.db.sql_list("""select item_group
from `tabWebsite Item Group` from `tabWebsite Item Group`
@ -203,7 +197,7 @@ class Item(WebsiteGenerator):
def make_route(self): def make_route(self):
if not self.route: if not self.route:
return cstr(frappe.db.get_value('Item Group', self.item_group, return cstr(frappe.db.get_value('Item Group', self.item_group,
'route')) + '/' + self.scrub((self.item_name if self.item_name else self.item_code) + '-' + random_string(5)) 'route')) + '/' + self.scrub((self.item_name or self.item_code) + '-' + random_string(5))
def validate_website_image(self): def validate_website_image(self):
if frappe.flags.in_import: if frappe.flags.in_import:
@ -258,7 +252,6 @@ class Item(WebsiteGenerator):
"attached_to_name": self.name "attached_to_name": self.name
}) })
except frappe.DoesNotExistError: except frappe.DoesNotExistError:
pass
# cleanup # cleanup
frappe.local.message_log.pop() frappe.local.message_log.pop()
@ -362,7 +355,9 @@ class Item(WebsiteGenerator):
context.update(get_slideshow(self)) context.update(get_slideshow(self))
def set_attribute_context(self, context): def set_attribute_context(self, context):
if self.has_variants: if not self.has_variants:
return
attribute_values_available = {} attribute_values_available = {}
context.attribute_values = {} context.attribute_values = {}
context.selected_attributes = {} context.selected_attributes = {}
@ -521,7 +516,7 @@ class Item(WebsiteGenerator):
def validate_item_type(self): def validate_item_type(self):
if self.has_serial_no == 1 and self.is_stock_item == 0 and not self.is_fixed_asset: if self.has_serial_no == 1 and self.is_stock_item == 0 and not self.is_fixed_asset:
msgprint(_("'Has Serial No' can not be 'Yes' for non-stock item"), raise_exception=1) frappe.throw(_("'Has Serial No' can not be 'Yes' for non-stock item"))
if self.has_serial_no == 0 and self.serial_no_series: if self.has_serial_no == 0 and self.serial_no_series:
self.serial_no_series = None self.serial_no_series = None
@ -542,10 +537,7 @@ class Item(WebsiteGenerator):
def fill_customer_code(self): def fill_customer_code(self):
""" Append all the customer codes and insert into "customer_code" field of item table """ """ Append all the customer codes and insert into "customer_code" field of item table """
cust_code = [] self.customer_code = ','.join(d.ref_code for d in self.get("customer_items", []))
for d in self.get('customer_items'):
cust_code.append(d.ref_code)
self.customer_code = ','.join(cust_code)
def check_item_tax(self): def check_item_tax(self):
"""Check whether Tax Rate is not entered twice for same Tax Type""" """Check whether Tax Rate is not entered twice for same Tax Type"""
@ -742,7 +734,9 @@ class Item(WebsiteGenerator):
def update_template_item(self): def update_template_item(self):
"""Set Show in Website for Template Item if True for its Variant""" """Set Show in Website for Template Item if True for its Variant"""
if self.variant_of: if not self.variant_of:
return
if self.show_in_website: if self.show_in_website:
self.show_variant_in_website = 1 self.show_variant_in_website = 1
self.show_in_website = 0 self.show_in_website = 0
@ -758,7 +752,7 @@ class Item(WebsiteGenerator):
template_item.save() template_item.save()
def validate_item_defaults(self): def validate_item_defaults(self):
companies = list(set([row.company for row in self.item_defaults])) companies = {row.company for row in self.item_defaults}
if len(companies) != len(self.item_defaults): if len(companies) != len(self.item_defaults):
frappe.throw(_("Cannot set multiple Item Defaults for a company.")) frappe.throw(_("Cannot set multiple Item Defaults for a company."))
@ -813,7 +807,7 @@ class Item(WebsiteGenerator):
frappe.throw(_("Item has variants.")) frappe.throw(_("Item has variants."))
def validate_attributes_in_variants(self): def validate_attributes_in_variants(self):
if not self.has_variants or self.get("__islocal"): if not self.has_variants or self.is_new():
return return
old_doc = self.get_doc_before_save() old_doc = self.get_doc_before_save()
@ -901,7 +895,7 @@ class Item(WebsiteGenerator):
frappe.throw(_("Variant Based On cannot be changed")) frappe.throw(_("Variant Based On cannot be changed"))
def validate_uom(self): def validate_uom(self):
if not self.get("__islocal"): if not self.is_new():
check_stock_uom_with_bin(self.name, self.stock_uom) check_stock_uom_with_bin(self.name, self.stock_uom)
if self.has_variants: if self.has_variants:
for d in frappe.db.get_all("Item", filters={"variant_of": self.name}): for d in frappe.db.get_all("Item", filters={"variant_of": self.name}):
@ -959,7 +953,9 @@ class Item(WebsiteGenerator):
d.variant_of = self.variant_of d.variant_of = self.variant_of
def cant_change(self): def cant_change(self):
if not self.get("__islocal"): if self.is_new():
return
fields = ("has_serial_no", "is_stock_item", "valuation_method", "has_batch_no") fields = ("has_serial_no", "is_stock_item", "valuation_method", "has_batch_no")
values = frappe.db.get_value("Item", self.name, fields, as_dict=True) values = frappe.db.get_value("Item", self.name, fields, as_dict=True)
@ -969,9 +965,7 @@ class Item(WebsiteGenerator):
if values: if values:
for field in fields: for field in fields:
if cstr(self.get(field)) != cstr(values.get(field)): if cstr(self.get(field)) != cstr(values.get(field)):
if not self.check_if_linked_document_exists(field): if self.check_if_linked_document_exists(field):
break # no linked document, allowed
else:
frappe.throw(_("As there are existing transactions against item {0}, you can not change the value of {1}").format(self.name, frappe.bold(self.meta.get_label(field)))) frappe.throw(_("As there are existing transactions against item {0}, you can not change the value of {1}").format(self.name, frappe.bold(self.meta.get_label(field))))
def check_if_linked_document_exists(self, field): def check_if_linked_document_exists(self, field):
@ -1054,56 +1048,42 @@ def make_item_price(item, price_list_name, item_price):
}).insert() }).insert()
def get_timeline_data(doctype, name): def get_timeline_data(doctype, name):
'''returns timeline data based on stock ledger entry''' """get timeline data based on Stock Ledger Entry. This is displayed as heatmap on the item page."""
out = {}
items = dict(frappe.db.sql('''select posting_date, count(*)
from `tabStock Ledger Entry` where item_code=%s
and posting_date > date_sub(curdate(), interval 1 year)
group by posting_date''', name))
for date, count in iteritems(items): items = frappe.db.sql("""select unix_timestamp(posting_date), count(*)
timestamp = get_timestamp(date) from `tabStock Ledger Entry`
out.update({timestamp: count}) where item_code=%s and posting_date > date_sub(curdate(), interval 1 year)
group by posting_date""", name)
return out return dict(items)
def validate_end_of_life(item_code, end_of_life=None, disabled=None, verbose=1):
def validate_end_of_life(item_code, end_of_life=None, disabled=None):
if (not end_of_life) or (disabled is None): if (not end_of_life) or (disabled is None):
end_of_life, disabled = frappe.db.get_value("Item", item_code, ["end_of_life", "disabled"]) end_of_life, disabled = frappe.db.get_value("Item", item_code, ["end_of_life", "disabled"])
if end_of_life and end_of_life != "0000-00-00" and getdate(end_of_life) <= now_datetime().date(): if end_of_life and end_of_life != "0000-00-00" and getdate(end_of_life) <= now_datetime().date():
msg = _("Item {0} has reached its end of life on {1}").format(item_code, formatdate(end_of_life)) frappe.throw(_("Item {0} has reached its end of life on {1}").format(item_code, formatdate(end_of_life)))
_msgprint(msg, verbose)
if disabled: if disabled:
_msgprint(_("Item {0} is disabled").format(item_code), verbose) frappe.throw(_("Item {0} is disabled").format(item_code))
def validate_is_stock_item(item_code, is_stock_item=None, verbose=1): def validate_is_stock_item(item_code, is_stock_item=None):
if not is_stock_item: if not is_stock_item:
is_stock_item = frappe.db.get_value("Item", item_code, "is_stock_item") is_stock_item = frappe.db.get_value("Item", item_code, "is_stock_item")
if is_stock_item != 1: if is_stock_item != 1:
msg = _("Item {0} is not a stock Item").format(item_code) frappe.throw(_("Item {0} is not a stock Item").format(item_code))
_msgprint(msg, verbose)
def validate_cancelled_item(item_code, docstatus=None, verbose=1): def validate_cancelled_item(item_code, docstatus=None):
if docstatus is None: if docstatus is None:
docstatus = frappe.db.get_value("Item", item_code, "docstatus") docstatus = frappe.db.get_value("Item", item_code, "docstatus")
if docstatus == 2: if docstatus == 2:
msg = _("Item {0} is cancelled").format(item_code) frappe.throw(_("Item {0} is cancelled").format(item_code))
_msgprint(msg, verbose)
def _msgprint(msg, verbose):
if verbose:
msgprint(msg, raise_exception=True)
else:
raise frappe.ValidationError(msg)
def get_last_purchase_details(item_code, doc_name=None, conversion_rate=1.0): def get_last_purchase_details(item_code, doc_name=None, conversion_rate=1.0):
"""returns last purchase details in stock uom""" """returns last purchase details in stock uom"""
@ -1203,27 +1183,25 @@ def check_stock_uom_with_bin(item, stock_uom):
if stock_uom == frappe.db.get_value("Item", item, "stock_uom"): if stock_uom == frappe.db.get_value("Item", item, "stock_uom"):
return return
matched = True
ref_uom = frappe.db.get_value("Stock Ledger Entry", ref_uom = frappe.db.get_value("Stock Ledger Entry",
{"item_code": item}, "stock_uom") {"item_code": item}, "stock_uom")
if ref_uom: if ref_uom:
if cstr(ref_uom) != cstr(stock_uom): if cstr(ref_uom) != cstr(stock_uom):
matched = False frappe.throw(_("Default Unit of Measure for Item {0} cannot be changed directly because you have already made some transaction(s) with another UOM. You will need to create a new Item to use a different Default UOM.").format(item))
else:
bin_list = frappe.db.sql("select * from tabBin where item_code=%s", item, as_dict=1)
for bin in bin_list:
if (bin.reserved_qty > 0 or bin.ordered_qty > 0 or bin.indented_qty > 0
or bin.planned_qty > 0) and cstr(bin.stock_uom) != cstr(stock_uom):
matched = False
break
if matched and bin_list: bin_list = frappe.db.sql("""
select * from tabBin where item_code = %s
and (reserved_qty > 0 or ordered_qty > 0 or indented_qty > 0 or planned_qty > 0)
and stock_uom != %s
""", (item, stock_uom), as_dict=1)
if bin_list:
frappe.throw(_("Default Unit of Measure for Item {0} cannot be changed directly because you have already made some transaction(s) with another UOM. You need to either cancel the linked documents or create a new Item.").format(item))
# No SLE or documents against item. Bin UOM can be changed safely.
frappe.db.sql("""update tabBin set stock_uom=%s where item_code=%s""", (stock_uom, item)) frappe.db.sql("""update tabBin set stock_uom=%s where item_code=%s""", (stock_uom, item))
if not matched:
frappe.throw(
_("Default Unit of Measure for Item {0} cannot be changed directly because you have already made some transaction(s) with another UOM. You will need to create a new Item to use a different Default UOM.").format(item))
def get_item_defaults(item_code, company): def get_item_defaults(item_code, company):
item = frappe.get_cached_doc('Item', item_code) item = frappe.get_cached_doc('Item', item_code)
@ -1264,45 +1242,59 @@ def get_item_details(item_code, company=None):
@frappe.whitelist() @frappe.whitelist()
def get_uom_conv_factor(uom, stock_uom): def get_uom_conv_factor(uom, stock_uom):
uoms = [uom, stock_uom] """ Get UOM conversion factor from uom to stock_uom
value = "" e.g. uom = "Kg", stock_uom = "Gram" then returns 1000.0
uom_details = frappe.db.sql("""select to_uom, from_uom, value from `tabUOM Conversion Factor`\ """
where to_uom in ({0}) if uom == stock_uom:
""".format(', '.join([frappe.db.escape(i, percent=False) for i in uoms])), as_dict=True) return 1.0
for d in uom_details: from_uom, to_uom = uom, stock_uom # renaming for readability
if d.from_uom == stock_uom and d.to_uom == uom:
value = 1/flt(d.value)
elif d.from_uom == uom and d.to_uom == stock_uom:
value = d.value
if not value: exact_match = frappe.db.get_value("UOM Conversion Factor", {"to_uom": to_uom, "from_uom": from_uom}, ["value"], as_dict=1)
uom_stock = frappe.db.get_value("UOM Conversion Factor", {"to_uom": stock_uom}, ["from_uom", "value"], as_dict=1) if exact_match:
uom_row = frappe.db.get_value("UOM Conversion Factor", {"to_uom": uom}, ["from_uom", "value"], as_dict=1) return exact_match.value
if uom_stock and uom_row: inverse_match = frappe.db.get_value("UOM Conversion Factor", {"to_uom": from_uom, "from_uom": to_uom}, ["value"], as_dict=1)
if uom_stock.from_uom == uom_row.from_uom: if inverse_match:
value = flt(uom_stock.value) * 1/flt(uom_row.value) return 1 / inverse_match.value
# This attempts to try and get conversion from intermediate UOM.
# case:
# g -> mg = 1000
# g -> kg = 0.001
# therefore kg -> mg = 1000 / 0.001 = 1,000,000
intermediate_match = frappe.db.sql("""
select (first.value / second.value) as value
from `tabUOM Conversion Factor` first
join `tabUOM Conversion Factor` second
on first.from_uom = second.from_uom
where
first.to_uom = %(to_uom)s
and second.to_uom = %(from_uom)s
limit 1
""", {"to_uom": to_uom, "from_uom": from_uom}, as_dict=1)
if intermediate_match:
return intermediate_match[0].value
return value
@frappe.whitelist() @frappe.whitelist()
def get_item_attribute(parent, attribute_value=''): def get_item_attribute(parent, attribute_value=""):
"""Used for providing auto-completions in child table."""
if not frappe.has_permission("Item"): if not frappe.has_permission("Item"):
frappe.msgprint(_("No Permission"), raise_exception=1) frappe.throw(_("No Permission"))
return frappe.get_all("Item Attribute Value", fields = ["attribute_value"], return frappe.get_all("Item Attribute Value", fields = ["attribute_value"],
filters = {'parent': parent, 'attribute_value': ("like", "%%%s%%" % attribute_value)}) filters = {'parent': parent, 'attribute_value': ("like", f"%{attribute_value}%")})
def update_variants(variants, template, publish_progress=True): def update_variants(variants, template, publish_progress=True):
count=0 total = len(variants)
for d in variants: for count, d in enumerate(variants, start=1):
variant = frappe.get_doc("Item", d) variant = frappe.get_doc("Item", d)
copy_attributes_to_variant(template, variant) copy_attributes_to_variant(template, variant)
variant.save() variant.save()
count+=1
if publish_progress: if publish_progress:
frappe.publish_progress(count*100/len(variants), title = _("Updating Variants...")) frappe.publish_progress(count / total * 100, title=_("Updating Variants..."))
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

View File

@ -10,14 +10,15 @@ from frappe.test_runner import make_test_objects
from erpnext.controllers.item_variant import (create_variant, ItemVariantExistsError, from erpnext.controllers.item_variant import (create_variant, ItemVariantExistsError,
InvalidItemAttributeValueError, get_variant) InvalidItemAttributeValueError, get_variant)
from erpnext.stock.doctype.item.item import StockExistsForTemplate, InvalidBarcode from erpnext.stock.doctype.item.item import StockExistsForTemplate, InvalidBarcode
from erpnext.stock.doctype.item.item import get_uom_conv_factor from erpnext.stock.doctype.item.item import (get_uom_conv_factor, get_item_attribute,
validate_is_stock_item, get_timeline_data)
from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry
from erpnext.stock.get_item_details import get_item_details from erpnext.stock.get_item_details import get_item_details
from erpnext.tests.utils import change_settings
from six import iteritems
test_ignore = ["BOM"] test_ignore = ["BOM"]
test_dependencies = ["Warehouse", "Item Group", "Item Tax Template", "Brand"] test_dependencies = ["Warehouse", "Item Group", "Item Tax Template", "Brand", "Item Attribute"]
def make_item(item_code, properties=None): def make_item(item_code, properties=None):
if frappe.db.exists("Item", item_code): if frappe.db.exists("Item", item_code):
@ -98,7 +99,7 @@ class TestItem(unittest.TestCase):
"ignore_pricing_rule": 1 "ignore_pricing_rule": 1
}) })
for key, value in iteritems(to_check): for key, value in to_check.items():
self.assertEqual(value, details.get(key)) self.assertEqual(value, details.get(key))
def test_item_tax_template(self): def test_item_tax_template(self):
@ -194,7 +195,7 @@ class TestItem(unittest.TestCase):
"plc_conversion_rate": 1, "plc_conversion_rate": 1,
"customer": "_Test Customer", "customer": "_Test Customer",
}) })
for key, value in iteritems(sales_item_check): for key, value in sales_item_check.items():
self.assertEqual(value, sales_item_details.get(key)) self.assertEqual(value, sales_item_details.get(key))
purchase_item_check = { purchase_item_check = {
@ -215,7 +216,7 @@ class TestItem(unittest.TestCase):
"plc_conversion_rate": 1, "plc_conversion_rate": 1,
"supplier": "_Test Supplier", "supplier": "_Test Supplier",
}) })
for key, value in iteritems(purchase_item_check): for key, value in purchase_item_check.items():
self.assertEqual(value, purchase_item_details.get(key)) self.assertEqual(value, purchase_item_details.get(key))
def test_item_attribute_change_after_variant(self): def test_item_attribute_change_after_variant(self):
@ -375,6 +376,14 @@ class TestItem(unittest.TestCase):
self.assertEqual(item_doc.uoms[1].uom, "Kg") self.assertEqual(item_doc.uoms[1].uom, "Kg")
self.assertEqual(item_doc.uoms[1].conversion_factor, 1000) self.assertEqual(item_doc.uoms[1].conversion_factor, 1000)
def test_uom_conv_intermediate(self):
factor = get_uom_conv_factor("Pound", "Gram")
self.assertAlmostEqual(factor, 453.592, 3)
def test_uom_conv_base_case(self):
factor = get_uom_conv_factor("m", "m")
self.assertEqual(factor, 1.0)
def test_item_variant_by_manufacturer(self): def test_item_variant_by_manufacturer(self):
fields = [{'field_name': 'description'}, {'field_name': 'variant_based_on'}] fields = [{'field_name': 'description'}, {'field_name': 'variant_based_on'}]
set_item_variant_settings(fields) set_item_variant_settings(fields)
@ -464,7 +473,7 @@ class TestItem(unittest.TestCase):
self.assertEqual(len(matching_barcodes), 1) self.assertEqual(len(matching_barcodes), 1)
details = matching_barcodes[0] details = matching_barcodes[0]
for key, value in iteritems(barcode_properties): for key, value in barcode_properties.items():
self.assertEqual(value, details.get(key)) self.assertEqual(value, details.get(key))
# Add barcode again - should cause DuplicateEntryError # Add barcode again - should cause DuplicateEntryError
@ -480,6 +489,89 @@ class TestItem(unittest.TestCase):
new_barcode.barcode_type = 'EAN' new_barcode.barcode_type = 'EAN'
self.assertRaises(InvalidBarcode, item_doc.save) self.assertRaises(InvalidBarcode, item_doc.save)
def test_heatmap_data(self):
import time
data = get_timeline_data("Item", "_Test Item")
self.assertTrue(isinstance(data, dict))
now = time.time()
one_year_ago = now - 366 * 24 * 60 * 60
for timestamp, count in data.items():
self.assertIsInstance(timestamp, int)
self.assertTrue(one_year_ago <= timestamp <= now)
self.assertIsInstance(count, int)
self.assertTrue(count >= 0)
def test_index_creation(self):
"check if index is getting created in db"
from erpnext.stock.doctype.item.item import on_doctype_update
on_doctype_update()
indices = frappe.db.sql("show index from tabItem", as_dict=1)
expected_columns = {"item_code", "item_name", "item_group", "route"}
for index in indices:
expected_columns.discard(index.get("Column_name"))
if expected_columns:
self.fail(f"Expected db index on these columns: {', '.join(expected_columns)}")
def test_attribute_completions(self):
expected_attrs = {"Small", "Extra Small", "Extra Large", "Large", "2XL", "Medium"}
attrs = get_item_attribute("Test Size")
received_attrs = {attr.attribute_value for attr in attrs}
self.assertEqual(received_attrs, expected_attrs)
attrs = get_item_attribute("Test Size", attribute_value="extra")
received_attrs = {attr.attribute_value for attr in attrs}
self.assertEqual(received_attrs, {"Extra Small", "Extra Large"})
def test_check_stock_uom_with_bin(self):
# this item has opening stock and stock_uom set in test_records.
item = frappe.get_doc("Item", "_Test Item")
item.stock_uom = "Gram"
self.assertRaises(frappe.ValidationError, item.save)
def test_check_stock_uom_with_bin_no_sle(self):
from erpnext.stock.stock_balance import update_bin_qty
item = create_item("_Item with bin qty")
item.stock_uom = "Gram"
item.save()
update_bin_qty(item.item_code, "_Test Warehouse - _TC", {
"reserved_qty": 10
})
item.stock_uom = "Kilometer"
self.assertRaises(frappe.ValidationError, item.save)
update_bin_qty(item.item_code, "_Test Warehouse - _TC", {
"reserved_qty": 0
})
item.load_from_db()
item.stock_uom = "Kilometer"
try:
item.save()
except frappe.ValidationError as e:
self.fail(f"UoM change not allowed even though no SLE / BIN with positive qty exists: {e}")
def test_validate_stock_item(self):
self.assertRaises(frappe.ValidationError, validate_is_stock_item, "_Test Non Stock Item")
try:
validate_is_stock_item("_Test Item")
except frappe.ValidationError as e:
self.fail(f"stock item considered non-stock item: {e}")
@change_settings("Stock Settings", {"item_naming_by": "Naming Series"})
def test_autoname_series(self):
item = frappe.new_doc("Item")
item.item_group = "All Item Groups"
item.save() # if item code saved without item_code then series worked
def set_item_variant_settings(fields): def set_item_variant_settings(fields):
doc = frappe.get_doc('Item Variant Settings') doc = frappe.get_doc('Item Variant Settings')
doc.set('fields', fields) doc.set('fields', fields)
@ -494,23 +586,24 @@ def make_item_variant():
test_records = frappe.get_test_records('Item') test_records = frappe.get_test_records('Item')
def create_item(item_code, is_stock_item=None, valuation_rate=0, warehouse=None, is_customer_provided_item=None, def create_item(item_code, is_stock_item=1, valuation_rate=0, warehouse="_Test Warehouse - _TC",
customer=None, is_purchase_item=None, opening_stock=None, company=None): is_customer_provided_item=None, customer=None, is_purchase_item=None, opening_stock=0,
company="_Test Company"):
if not frappe.db.exists("Item", item_code): if not frappe.db.exists("Item", item_code):
item = frappe.new_doc("Item") item = frappe.new_doc("Item")
item.item_code = item_code item.item_code = item_code
item.item_name = item_code item.item_name = item_code
item.description = item_code item.description = item_code
item.item_group = "All Item Groups" item.item_group = "All Item Groups"
item.is_stock_item = is_stock_item or 1 item.is_stock_item = is_stock_item
item.opening_stock = opening_stock or 0 item.opening_stock = opening_stock
item.valuation_rate = valuation_rate or 0.0 item.valuation_rate = valuation_rate
item.is_purchase_item = is_purchase_item item.is_purchase_item = is_purchase_item
item.is_customer_provided_item = is_customer_provided_item item.is_customer_provided_item = is_customer_provided_item
item.customer = customer or '' item.customer = customer or ''
item.append("item_defaults", { item.append("item_defaults", {
"default_warehouse": warehouse or '_Test Warehouse - _TC', "default_warehouse": warehouse,
"company": company or "_Test Company" "company": company
}) })
item.save() item.save()
else: else:

Some files were not shown because too many files have changed in this diff Show More