diff --git a/.eslintrc b/.eslintrc
index e40502acd6..cb45ce5f69 100644
--- a/.eslintrc
+++ b/.eslintrc
@@ -147,11 +147,14 @@
"Chart": true,
"Cypress": true,
"cy": true,
+ "describe": true,
+ "expect": true,
"it": true,
"context": true,
"before": true,
"beforeEach": true,
"onScan": true,
- "extend_cscript": true
+ "extend_cscript": true,
+ "localforage": true,
}
}
diff --git a/.github/helper/install.sh b/.github/helper/install.sh
index f7a7122343..455ab861f9 100644
--- a/.github/helper/install.sh
+++ b/.github/helper/install.sh
@@ -42,6 +42,6 @@ sed -i 's/socketio:/# socketio:/g' Procfile
sed -i 's/redis_socketio:/# redis_socketio:/g' Procfile
bench get-app erpnext "${GITHUB_WORKSPACE}"
-bench start &
+bench start &> bench_run_logs.txt &
bench --site test_site reinstall --yes
bench build --app frappe
diff --git a/.github/workflows/ui-tests.yml b/.github/workflows/ui-tests.yml
new file mode 100644
index 0000000000..412a05b0a1
--- /dev/null
+++ b/.github/workflows/ui-tests.yml
@@ -0,0 +1,108 @@
+name: UI
+
+on:
+ pull_request:
+ workflow_dispatch:
+
+jobs:
+ test:
+ runs-on: ubuntu-18.04
+
+ strategy:
+ fail-fast: false
+
+ name: UI Tests (Cypress)
+
+ 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.7
+
+ - uses: actions/setup-node@v2
+ with:
+ node-version: 14
+ check-latest: true
+
+ - 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: Cache cypress binary
+ uses: actions/cache@v2
+ with:
+ path: ~/.cache
+ key: ${{ runner.os }}-cypress-
+ restore-keys: |
+ ${{ runner.os }}-cypress-
+ ${{ runner.os }}-
+
+ - name: Install
+ run: bash ${GITHUB_WORKSPACE}/.github/helper/install.sh
+ env:
+ DB: mariadb
+ TYPE: ui
+
+ - name: Site Setup
+ run: cd ~/frappe-bench/ && bench --site test_site execute erpnext.setup.utils.before_tests
+
+ - name: cypress pre-requisites
+ run: cd ~/frappe-bench/apps/frappe && yarn add cypress-file-upload@^5 --no-lockfile
+
+
+ - name: Build Assets
+ run: cd ~/frappe-bench/ && bench build
+
+ - name: UI Tests
+ run: cd ~/frappe-bench/ && bench --site test_site run-ui-tests erpnext --headless
+ env:
+ CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }}
+
+ - name: Show bench console if tests failed
+ if: ${{ failure() }}
+ run: cat ~/frappe-bench/bench_run_logs.txt
diff --git a/.gitignore b/.gitignore
index 63c51c4976..89f56263ff 100644
--- a/.gitignore
+++ b/.gitignore
@@ -16,3 +16,4 @@ __pycache__
.idea/
.vscode/
node_modules/
+.backportrc.json
\ No newline at end of file
diff --git a/cypress.json b/cypress.json
new file mode 100644
index 0000000000..02b10d893f
--- /dev/null
+++ b/cypress.json
@@ -0,0 +1,11 @@
+{
+ "baseUrl": "http://test_site:8000/",
+ "projectId": "da59y9",
+ "adminPassword": "admin",
+ "defaultCommandTimeout": 20000,
+ "pageLoadTimeout": 15000,
+ "retries": {
+ "runMode": 2,
+ "openMode": 2
+ }
+}
diff --git a/cypress/fixtures/example.json b/cypress/fixtures/example.json
new file mode 100644
index 0000000000..da18d9352a
--- /dev/null
+++ b/cypress/fixtures/example.json
@@ -0,0 +1,5 @@
+{
+ "name": "Using fixtures to represent data",
+ "email": "hello@cypress.io",
+ "body": "Fixtures are a great way to mock data for responses to routes"
+}
\ No newline at end of file
diff --git a/cypress/integration/test_customer.js b/cypress/integration/test_customer.js
new file mode 100644
index 0000000000..3d6ed5d0d8
--- /dev/null
+++ b/cypress/integration/test_customer.js
@@ -0,0 +1,13 @@
+
+context('Customer', () => {
+ before(() => {
+ cy.login();
+ });
+ it('Check Customer Group', () => {
+ cy.visit(`app/customer/`);
+ cy.get('.primary-action').click();
+ cy.wait(500);
+ cy.get('.custom-actions > .btn').click();
+ cy.get_field('customer_group', 'Link').should('have.value', 'All Customer Groups');
+ });
+});
diff --git a/cypress/integration/test_item.js b/cypress/integration/test_item.js
new file mode 100644
index 0000000000..fcb7533a22
--- /dev/null
+++ b/cypress/integration/test_item.js
@@ -0,0 +1,44 @@
+describe("Test Item Dashboard", () => {
+ before(() => {
+ cy.login();
+ cy.visit("/app/item");
+ cy.insert_doc(
+ "Item",
+ {
+ item_code: "e2e_test_item",
+ item_group: "All Item Groups",
+ opening_stock: 42,
+ valuation_rate: 100,
+ },
+ true
+ );
+ cy.go_to_doc("item", "e2e_test_item");
+ });
+
+ it("should show dashboard with correct data on first load", () => {
+ cy.get(".stock-levels").contains("Stock Levels").should("be.visible");
+ cy.get(".stock-levels").contains("e2e_test_item").should("exist");
+
+ // reserved and available qty
+ cy.get(".stock-levels .inline-graph-count")
+ .eq(0)
+ .contains("0")
+ .should("exist");
+ cy.get(".stock-levels .inline-graph-count")
+ .eq(1)
+ .contains("42")
+ .should("exist");
+ });
+
+ it("should persist on field change", () => {
+ cy.get('input[data-fieldname="disabled"]').check();
+ cy.wait(500);
+ cy.get(".stock-levels").contains("Stock Levels").should("be.visible");
+ cy.get(".stock-levels").should("have.length", 1);
+ });
+
+ it("should persist on reload", () => {
+ cy.reload();
+ cy.get(".stock-levels").contains("Stock Levels").should("be.visible");
+ });
+});
diff --git a/cypress/plugins/index.js b/cypress/plugins/index.js
new file mode 100644
index 0000000000..07d9804a73
--- /dev/null
+++ b/cypress/plugins/index.js
@@ -0,0 +1,17 @@
+// ***********************************************************
+// This example plugins/index.js can be used to load plugins
+//
+// You can change the location of this file or turn off loading
+// the plugins file with the 'pluginsFile' configuration option.
+//
+// You can read more here:
+// https://on.cypress.io/plugins-guide
+// ***********************************************************
+
+// This function is called when a project is opened or re-opened (e.g. due to
+// the project's config changing)
+
+module.exports = () => {
+ // `on` is used to hook into various events Cypress emits
+ // `config` is the resolved Cypress config
+};
diff --git a/cypress/support/commands.js b/cypress/support/commands.js
new file mode 100644
index 0000000000..7ddc80ab8d
--- /dev/null
+++ b/cypress/support/commands.js
@@ -0,0 +1,31 @@
+// ***********************************************
+// This example commands.js shows you how to
+// create various custom commands and overwrite
+// existing commands.
+//
+// For more comprehensive examples of custom
+// commands please read more here:
+// https://on.cypress.io/custom-commands
+// ***********************************************
+//
+//
+// -- This is a parent command --
+// Cypress.Commands.add("login", (email, password) => { ... });
+//
+//
+// -- This is a child command --
+// Cypress.Commands.add("drag", { prevSubject: 'element'}, (subject, options) => { ... });
+//
+//
+// -- This is a dual command --
+// Cypress.Commands.add("dismiss", { prevSubject: 'optional'}, (subject, options) => { ... });
+//
+//
+// -- This is will overwrite an existing command --
+// Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... });
+
+const slug = (name) => name.toLowerCase().replace(" ", "-");
+
+Cypress.Commands.add("go_to_doc", (doctype, name) => {
+ cy.visit(`/app/${slug(doctype)}/${encodeURIComponent(name)}`);
+});
diff --git a/cypress/support/index.js b/cypress/support/index.js
new file mode 100644
index 0000000000..72070cc81c
--- /dev/null
+++ b/cypress/support/index.js
@@ -0,0 +1,26 @@
+// ***********************************************************
+// This example support/index.js is processed and
+// loaded automatically before your test files.
+//
+// This is a great place to put global configuration and
+// behavior that modifies Cypress.
+//
+// You can change the location of this file or turn off
+// automatically serving support files with the
+// 'supportFile' configuration option.
+//
+// You can read more here:
+// https://on.cypress.io/configuration
+// ***********************************************************
+
+// Import commands.js using ES2015 syntax:
+import './commands';
+import '../../../frappe/cypress/support/commands' // eslint-disable-line
+
+
+// Alternatively you can use CommonJS syntax:
+// require('./commands')
+
+Cypress.Cookies.defaults({
+ preserve: 'sid'
+});
diff --git a/cypress/tsconfig.json b/cypress/tsconfig.json
new file mode 100644
index 0000000000..d90ebf6856
--- /dev/null
+++ b/cypress/tsconfig.json
@@ -0,0 +1,12 @@
+{
+ "compilerOptions": {
+ "allowJs": true,
+ "baseUrl": "../node_modules",
+ "types": [
+ "cypress"
+ ]
+ },
+ "include": [
+ "**/*.*"
+ ]
+}
\ No newline at end of file
diff --git a/erpnext/__init__.py b/erpnext/__init__.py
index 39d9a27615..0c96d325c2 100644
--- a/erpnext/__init__.py
+++ b/erpnext/__init__.py
@@ -5,7 +5,7 @@ import frappe
from erpnext.hooks import regional_overrides
from frappe.utils import getdate
-__version__ = '13.5.2'
+__version__ = '13.6.0'
def get_default_company(user=None):
'''Get default company for user'''
diff --git a/erpnext/accounts/deferred_revenue.py b/erpnext/accounts/deferred_revenue.py
index 2f86c6c1de..335e8a15ab 100644
--- a/erpnext/accounts/deferred_revenue.py
+++ b/erpnext/accounts/deferred_revenue.py
@@ -301,17 +301,21 @@ def process_deferred_accounting(posting_date=None):
start_date = add_months(today(), -1)
end_date = add_days(today(), -1)
- for record_type in ('Income', 'Expense'):
- doc = frappe.get_doc(dict(
- doctype='Process Deferred Accounting',
- posting_date=posting_date,
- start_date=start_date,
- end_date=end_date,
- type=record_type
- ))
+ companies = frappe.get_all('Company')
- doc.insert()
- doc.submit()
+ for company in companies:
+ for record_type in ('Income', 'Expense'):
+ doc = frappe.get_doc(dict(
+ doctype='Process Deferred Accounting',
+ company=company.name,
+ posting_date=posting_date,
+ start_date=start_date,
+ end_date=end_date,
+ type=record_type
+ ))
+
+ doc.insert()
+ doc.submit()
def make_gl_entries(doc, credit_account, debit_account, against,
amount, base_amount, posting_date, project, account_currency, cost_center, item, deferred_process=None):
diff --git a/erpnext/accounts/doctype/accounting_dimension/accounting_dimension.py b/erpnext/accounts/doctype/accounting_dimension/accounting_dimension.py
index 7cd1e7736c..fac28c9239 100644
--- a/erpnext/accounts/doctype/accounting_dimension/accounting_dimension.py
+++ b/erpnext/accounts/doctype/accounting_dimension/accounting_dimension.py
@@ -19,7 +19,7 @@ class AccountingDimension(Document):
def validate(self):
if self.document_type in core_doctypes_list + ('Accounting Dimension', 'Project',
- 'Cost Center', 'Accounting Dimension Detail', 'Company') :
+ 'Cost Center', 'Accounting Dimension Detail', 'Company', 'Account') :
msg = _("Not allowed to create accounting dimension for {0}").format(self.document_type)
frappe.throw(msg)
diff --git a/erpnext/accounts/doctype/bank_statement_import/bank_statement_import.py b/erpnext/accounts/doctype/bank_statement_import/bank_statement_import.py
index 5f110e2727..ffc9d1c465 100644
--- a/erpnext/accounts/doctype/bank_statement_import/bank_statement_import.py
+++ b/erpnext/accounts/doctype/bank_statement_import/bank_statement_import.py
@@ -51,7 +51,7 @@ class BankStatementImport(DataImport):
self.import_file, self.google_sheets_url
)
- if 'Bank Account' not in json.dumps(preview):
+ if 'Bank Account' not in json.dumps(preview['columns']):
frappe.throw(_("Please add the Bank Account column"))
from frappe.core.page.background_jobs.background_jobs import get_info
diff --git a/erpnext/accounts/doctype/dunning/dunning.py b/erpnext/accounts/doctype/dunning/dunning.py
index 7c1a1717da..c6c689212b 100644
--- a/erpnext/accounts/doctype/dunning/dunning.py
+++ b/erpnext/accounts/doctype/dunning/dunning.py
@@ -96,7 +96,7 @@ def calculate_interest_and_amount(posting_date, outstanding_amount, rate_of_inte
grand_total = 0
if rate_of_interest:
interest_per_year = flt(outstanding_amount) * flt(rate_of_interest) / 100
- interest_amount = (interest_per_year * cint(overdue_days)) / 365
+ interest_amount = (interest_per_year * cint(overdue_days)) / 365
grand_total = flt(outstanding_amount) + flt(interest_amount) + flt(dunning_fee)
dunning_amount = flt(interest_amount) + flt(dunning_fee)
return {
diff --git a/erpnext/accounts/doctype/gl_entry/gl_entry.py b/erpnext/accounts/doctype/gl_entry/gl_entry.py
index 948c51364e..11465b711e 100644
--- a/erpnext/accounts/doctype/gl_entry/gl_entry.py
+++ b/erpnext/accounts/doctype/gl_entry/gl_entry.py
@@ -121,8 +121,7 @@ class GLEntry(Document):
def check_pl_account(self):
if self.is_opening=='Yes' and \
- frappe.db.get_value("Account", self.account, "report_type")=="Profit and Loss" and \
- self.voucher_type not in ['Purchase Invoice', 'Sales Invoice']:
+ frappe.db.get_value("Account", self.account, "report_type")=="Profit and Loss":
frappe.throw(_("{0} {1}: 'Profit and Loss' type account {2} not allowed in Opening Entry")
.format(self.voucher_type, self.voucher_no, self.account))
diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py
index adaf99a790..0c21aae944 100644
--- a/erpnext/accounts/doctype/payment_entry/payment_entry.py
+++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py
@@ -1318,9 +1318,9 @@ def get_reference_details(reference_doctype, reference_name, party_account_curre
return frappe._dict({
"due_date": ref_doc.get("due_date"),
- "total_amount": total_amount,
- "outstanding_amount": outstanding_amount,
- "exchange_rate": exchange_rate,
+ "total_amount": flt(total_amount),
+ "outstanding_amount": flt(outstanding_amount),
+ "exchange_rate": flt(exchange_rate),
"bill_no": bill_no
})
diff --git a/erpnext/accounts/doctype/payment_entry/test_payment_entry.py b/erpnext/accounts/doctype/payment_entry/test_payment_entry.py
index 4641d6b5ff..d1302f5ae7 100644
--- a/erpnext/accounts/doctype/payment_entry/test_payment_entry.py
+++ b/erpnext/accounts/doctype/payment_entry/test_payment_entry.py
@@ -589,9 +589,9 @@ class TestPaymentEntry(unittest.TestCase):
party_account_balance = get_balance_on(account=pe.paid_from, cost_center=pe.cost_center)
self.assertEqual(pe.cost_center, si.cost_center)
- self.assertEqual(expected_account_balance, account_balance)
- self.assertEqual(expected_party_balance, party_balance)
- self.assertEqual(expected_party_account_balance, party_account_balance)
+ self.assertEqual(flt(expected_account_balance), account_balance)
+ self.assertEqual(flt(expected_party_balance), party_balance)
+ self.assertEqual(flt(expected_party_account_balance), party_account_balance)
def create_payment_terms_template():
diff --git a/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py b/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py
index 0b0ee904ff..500952e38a 100644
--- a/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py
+++ b/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py
@@ -207,10 +207,9 @@ def fetch_customers(customer_collection, collection_name, primary_mandatory):
@frappe.whitelist()
def get_customer_emails(customer_name, primary_mandatory, billing_and_primary=True):
billing_email = frappe.db.sql("""
- SELECT c.email_id FROM `tabContact` AS c JOIN `tabDynamic Link` AS l ON c.name=l.parent \
- WHERE l.link_doctype='Customer' and l.link_name='""" + customer_name + """' and \
- c.is_billing_contact=1 \
- order by c.creation desc""")
+ SELECT c.email_id FROM `tabContact` AS c JOIN `tabDynamic Link` AS l ON c.name=l.parent
+ WHERE l.link_doctype='Customer' and l.link_name=%s and c.is_billing_contact=1
+ order by c.creation desc""", customer_name)
if len(billing_email) == 0 or (billing_email[0][0] is None):
if billing_and_primary:
diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py
index 45d89ad1c8..c1cc092554 100644
--- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py
+++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py
@@ -517,6 +517,8 @@ class PurchaseInvoice(BuyingController):
if d.category in ('Valuation', 'Total and Valuation')
and flt(d.base_tax_amount_after_discount_amount)]
+ exchange_rate_map, net_rate_map = get_purchase_document_details(self)
+
for item in self.get("items"):
if flt(item.base_net_amount):
account_currency = get_account_currency(item.expense_account)
@@ -634,6 +636,34 @@ class PurchaseInvoice(BuyingController):
"project": item.project or self.project
}, account_currency, item=item))
+ # check if the exchange rate has changed
+ if item.get('purchase_receipt'):
+ if exchange_rate_map[item.purchase_receipt] and \
+ self.conversion_rate != exchange_rate_map[item.purchase_receipt] and \
+ item.net_rate == net_rate_map[item.pr_detail]:
+
+ discrepancy_caused_by_exchange_rate_difference = (item.qty * item.net_rate) * \
+ (exchange_rate_map[item.purchase_receipt] - self.conversion_rate)
+
+ gl_entries.append(
+ self.get_gl_dict({
+ "account": expense_account,
+ "against": self.supplier,
+ "debit": discrepancy_caused_by_exchange_rate_difference,
+ "cost_center": item.cost_center,
+ "project": item.project or self.project
+ }, account_currency, item=item)
+ )
+ gl_entries.append(
+ self.get_gl_dict({
+ "account": self.get_company_default("exchange_gain_loss_account"),
+ "against": self.supplier,
+ "credit": discrepancy_caused_by_exchange_rate_difference,
+ "cost_center": item.cost_center,
+ "project": item.project or self.project
+ }, account_currency, item=item)
+ )
+
# If asset is bought through this document and not linked to PR
if self.update_stock and item.landed_cost_voucher_amount:
expenses_included_in_asset_valuation = self.get_company_default("expenses_included_in_asset_valuation")
@@ -1141,6 +1171,36 @@ class PurchaseInvoice(BuyingController):
if update:
self.db_set('status', self.status, update_modified = update_modified)
+# to get details of purchase invoice/receipt from which this doc was created for exchange rate difference handling
+def get_purchase_document_details(doc):
+ if doc.doctype == 'Purchase Invoice':
+ doc_reference = 'purchase_receipt'
+ items_reference = 'pr_detail'
+ parent_doctype = 'Purchase Receipt'
+ child_doctype = 'Purchase Receipt Item'
+ else:
+ doc_reference = 'purchase_invoice'
+ items_reference = 'purchase_invoice_item'
+ parent_doctype = 'Purchase Invoice'
+ child_doctype = 'Purchase Invoice Item'
+
+ purchase_receipts_or_invoices = []
+ items = []
+
+ for item in doc.get('items'):
+ if item.get(doc_reference):
+ purchase_receipts_or_invoices.append(item.get(doc_reference))
+ if item.get(items_reference):
+ items.append(item.get(items_reference))
+
+ exchange_rate_map = frappe._dict(frappe.get_all(parent_doctype, filters={'name': ('in',
+ purchase_receipts_or_invoices)}, fields=['name', 'conversion_rate'], as_list=1))
+
+ net_rate_map = frappe._dict(frappe.get_all(child_doctype, filters={'name': ('in',
+ items)}, fields=['name', 'net_rate'], as_list=1))
+
+ return exchange_rate_map, net_rate_map
+
def get_list_context(context=None):
from erpnext.controllers.website_list_for_contact import get_list_context
list_context = get_list_context(context)
diff --git a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py
index 2f5d36c8fa..189260a29d 100644
--- a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py
+++ b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py
@@ -230,6 +230,27 @@ class TestPurchaseInvoice(unittest.TestCase):
self.assertEqual(expected_values[gle.account][1], gle.debit)
self.assertEqual(expected_values[gle.account][2], gle.credit)
+ def test_purchase_invoice_with_exchange_rate_difference(self):
+ from erpnext.stock.doctype.purchase_receipt.purchase_receipt import make_purchase_invoice as create_purchase_invoice
+
+ pr = make_purchase_receipt(company="_Test Company with perpetual inventory", warehouse='Stores - TCP1',
+ currency = "USD", conversion_rate = 70)
+
+ pi = create_purchase_invoice(pr.name)
+ pi.conversion_rate = 80
+
+ pi.insert()
+ pi.submit()
+
+ # Get exchnage gain and loss account
+ exchange_gain_loss_account = frappe.db.get_value('Company', pi.company, 'exchange_gain_loss_account')
+
+ # fetching the latest GL Entry with exchange gain and loss account account
+ amount = frappe.db.get_value('GL Entry', {'account': exchange_gain_loss_account, 'voucher_no': pi.name}, 'debit')
+ discrepancy_caused_by_exchange_rate_diff = abs(pi.items[0].base_net_amount - pr.items[0].base_net_amount)
+
+ self.assertEqual(discrepancy_caused_by_exchange_rate_diff, amount)
+
def test_purchase_invoice_change_naming_series(self):
pi = frappe.copy_doc(test_records[1])
pi.insert()
@@ -1010,21 +1031,21 @@ class TestPurchaseInvoice(unittest.TestCase):
# Check GLE for Purchase Invoice
# Zero net effect on final TDS Payable on invoice
expected_gle = [
- ['_Test Account Cost for Goods Sold - _TC', 30000, 0],
- ['_Test Account Excise Duty - _TC', 0, 3000],
- ['Creditors - _TC', 0, 27000],
- ['TDS Payable - _TC', 3000, 3000]
+ ['_Test Account Cost for Goods Sold - _TC', 30000],
+ ['_Test Account Excise Duty - _TC', -3000],
+ ['Creditors - _TC', -27000],
+ ['TDS Payable - _TC', 0]
]
- gl_entries = frappe.db.sql("""select account, debit, credit
+ gl_entries = frappe.db.sql("""select account, sum(debit - credit) as amount
from `tabGL Entry`
where voucher_type='Purchase Invoice' and voucher_no=%s
+ group by account
order by account asc""", (purchase_invoice.name), as_dict=1)
for i, gle in enumerate(gl_entries):
self.assertEqual(expected_gle[i][0], gle.account)
- self.assertEqual(expected_gle[i][1], gle.debit)
- self.assertEqual(expected_gle[i][2], gle.credit)
+ self.assertEqual(expected_gle[i][1], gle.amount)
def update_tax_witholding_category(company, account, date):
from erpnext.accounts.utils import get_fiscal_year
diff --git a/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json b/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json
index 29f72470b3..b39022dd75 100644
--- a/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json
+++ b/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json
@@ -854,7 +854,7 @@
"idx": 1,
"istable": 1,
"links": [],
- "modified": "2021-06-16 19:33:51.099386",
+ "modified": "2021-06-16 19:43:51.099386",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Purchase Invoice Item",
diff --git a/erpnext/accounts/party.py b/erpnext/accounts/party.py
index e025fc6905..b97dc401e6 100644
--- a/erpnext/accounts/party.py
+++ b/erpnext/accounts/party.py
@@ -542,6 +542,7 @@ def get_dashboard_info(party_type, party, loyalty_program=None):
select company, sum(debit_in_account_currency) - sum(credit_in_account_currency)
from `tabGL Entry`
where party_type = %s and party=%s
+ and is_cancelled = 0
group by company""", (party_type, party)))
for d in companies:
diff --git a/erpnext/accounts/report/budget_variance_report/budget_variance_report.py b/erpnext/accounts/report/budget_variance_report/budget_variance_report.py
index 9c9ada871c..f1b231b690 100644
--- a/erpnext/accounts/report/budget_variance_report/budget_variance_report.py
+++ b/erpnext/accounts/report/budget_variance_report/budget_variance_report.py
@@ -397,6 +397,7 @@ def get_chart_data(filters, columns, data):
{'name': 'Budget', 'chartType': 'bar', 'values': budget_values},
{'name': 'Actual Expense', 'chartType': 'bar', 'values': actual_values}
]
- }
+ },
+ 'type' : 'bar'
}
diff --git a/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py b/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py
index 7793af737f..56a67bb098 100644
--- a/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py
+++ b/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py
@@ -380,7 +380,7 @@ def set_gl_entries_by_account(from_date, to_date, root_lft, root_rgt, filters, g
gl_entries = frappe.db.sql("""select gl.posting_date, gl.account, gl.debit, gl.credit, gl.is_opening, gl.company,
gl.fiscal_year, gl.debit_in_account_currency, gl.credit_in_account_currency, gl.account_currency,
acc.account_name, acc.account_number
- from `tabGL Entry` gl, `tabAccount` acc where acc.name = gl.account and gl.company = %(company)s
+ from `tabGL Entry` gl, `tabAccount` acc where acc.name = gl.account and gl.company = %(company)s and gl.is_cancelled = 0
{additional_conditions} and gl.posting_date <= %(to_date)s and acc.lft >= %(lft)s and acc.rgt <= %(rgt)s
order by gl.account, gl.posting_date""".format(additional_conditions=additional_conditions),
{
diff --git a/erpnext/accounts/report/general_ledger/general_ledger.js b/erpnext/accounts/report/general_ledger/general_ledger.js
index 84f786814d..4a551b8012 100644
--- a/erpnext/accounts/report/general_ledger/general_ledger.js
+++ b/erpnext/accounts/report/general_ledger/general_ledger.js
@@ -36,16 +36,12 @@ frappe.query_reports["General Ledger"] = {
{
"fieldname":"account",
"label": __("Account"),
- "fieldtype": "Link",
+ "fieldtype": "MultiSelectList",
"options": "Account",
- "get_query": function() {
- var company = frappe.query_report.get_filter_value('company');
- return {
- "doctype": "Account",
- "filters": {
- "company": company,
- }
- }
+ get_data: function(txt) {
+ return frappe.db.get_link_options('Account', txt, {
+ company: frappe.query_report.get_filter_value("company")
+ });
}
},
{
@@ -135,7 +131,9 @@ frappe.query_reports["General Ledger"] = {
"label": __("Cost Center"),
"fieldtype": "MultiSelectList",
get_data: function(txt) {
- return frappe.db.get_link_options('Cost Center', txt);
+ return frappe.db.get_link_options('Cost Center', txt, {
+ company: frappe.query_report.get_filter_value("company")
+ });
}
},
{
@@ -143,7 +141,9 @@ frappe.query_reports["General Ledger"] = {
"label": __("Project"),
"fieldtype": "MultiSelectList",
get_data: function(txt) {
- return frappe.db.get_link_options('Project', txt);
+ return frappe.db.get_link_options('Project', txt, {
+ company: frappe.query_report.get_filter_value("company")
+ });
}
},
{
diff --git a/erpnext/accounts/report/general_ledger/general_ledger.py b/erpnext/accounts/report/general_ledger/general_ledger.py
index 562df4f6f7..744ada9e55 100644
--- a/erpnext/accounts/report/general_ledger/general_ledger.py
+++ b/erpnext/accounts/report/general_ledger/general_ledger.py
@@ -49,8 +49,12 @@ def validate_filters(filters, account_details):
if not filters.get("from_date") and not filters.get("to_date"):
frappe.throw(_("{0} and {1} are mandatory").format(frappe.bold(_("From Date")), frappe.bold(_("To Date"))))
- if filters.get("account") and not account_details.get(filters.account):
- frappe.throw(_("Account {0} does not exists").format(filters.account))
+ for account in filters.account:
+ if not account_details.get(account):
+ frappe.throw(_("Account {0} does not exists").format(account))
+
+ if filters.get('account'):
+ filters.account = frappe.parse_json(filters.get('account'))
if (filters.get("account") and filters.get("group_by") == _('Group by Account')
and account_details[filters.account].is_group == 0):
@@ -87,7 +91,19 @@ def set_account_currency(filters):
account_currency = None
if filters.get("account"):
- account_currency = get_account_currency(filters.account)
+ if len(filters.get("account")) == 1:
+ account_currency = get_account_currency(filters.account[0])
+ else:
+ currency = get_account_currency(filters.account[0])
+ is_same_account_currency = True
+ for account in filters.get("account"):
+ if get_account_currency(account) != currency:
+ is_same_account_currency = False
+ break
+
+ if is_same_account_currency:
+ account_currency = currency
+
elif filters.get("party"):
gle_currency = frappe.db.get_value(
"GL Entry", {
@@ -205,10 +221,10 @@ def get_gl_entries(filters, accounting_dimensions):
def get_conditions(filters):
conditions = []
- if filters.get("account") and not filters.get("include_dimensions"):
- lft, rgt = frappe.db.get_value("Account", filters["account"], ["lft", "rgt"])
- conditions.append("""account in (select name from tabAccount
- where lft>=%s and rgt<=%s and docstatus<2)""" % (lft, rgt))
+
+ if filters.get("account"):
+ filters.account = get_accounts_with_children(filters.account)
+ conditions.append("account in %(account)s")
if filters.get("cost_center"):
filters.cost_center = get_cost_centers_with_children(filters.cost_center)
@@ -266,6 +282,20 @@ def get_conditions(filters):
return "and {}".format(" and ".join(conditions)) if conditions else ""
+def get_accounts_with_children(accounts):
+ if not isinstance(accounts, list):
+ accounts = [d.strip() for d in accounts.strip().split(',') if d]
+
+ all_accounts = []
+ for d in accounts:
+ if frappe.db.exists("Account", d):
+ lft, rgt = frappe.db.get_value("Account", d, ["lft", "rgt"])
+ children = frappe.get_all("Account", filters={"lft": [">=", lft], "rgt": ["<=", rgt]})
+ all_accounts += [c.name for c in children]
+ else:
+ frappe.throw(_("Account: {0} does not exist").format(d))
+
+ return list(set(all_accounts))
def get_data_with_opening_closing(filters, account_details, accounting_dimensions, gl_entries):
data = []
diff --git a/erpnext/assets/doctype/asset/asset.js b/erpnext/assets/doctype/asset/asset.js
index 6f1bb28f37..922cc4a7b2 100644
--- a/erpnext/assets/doctype/asset/asset.js
+++ b/erpnext/assets/doctype/asset/asset.js
@@ -82,24 +82,46 @@ frappe.ui.form.on('Asset', {
if (in_list(["Submitted", "Partially Depreciated", "Fully Depreciated"], frm.doc.status)) {
frm.add_custom_button("Transfer Asset", function() {
erpnext.asset.transfer_asset(frm);
- });
+ }, __("Manage"));
frm.add_custom_button("Scrap Asset", function() {
erpnext.asset.scrap_asset(frm);
- });
+ }, __("Manage"));
frm.add_custom_button("Sell Asset", function() {
frm.trigger("make_sales_invoice");
- });
+ }, __("Manage"));
} else if (frm.doc.status=='Scrapped') {
frm.add_custom_button("Restore Asset", function() {
erpnext.asset.restore_asset(frm);
- });
+ }, __("Manage"));
+ }
+
+ if (frm.doc.maintenance_required && !frm.doc.maintenance_schedule) {
+ frm.add_custom_button(__("Maintain Asset"), function() {
+ frm.trigger("create_asset_maintenance");
+ }, __("Manage"));
+ }
+
+ frm.add_custom_button(__("Repair Asset"), function() {
+ frm.trigger("create_asset_repair");
+ }, __("Manage"));
+
+ if (frm.doc.status != 'Fully Depreciated') {
+ frm.add_custom_button(__("Adjust Asset Value"), function() {
+ frm.trigger("create_asset_adjustment");
+ }, __("Manage"));
+ }
+
+ if (!frm.doc.calculate_depreciation) {
+ frm.add_custom_button(__("Create Depreciation Entry"), function() {
+ frm.trigger("make_journal_entry");
+ }, __("Manage"));
}
if (frm.doc.purchase_receipt || !frm.doc.is_existing_asset) {
- frm.add_custom_button("General Ledger", function() {
+ frm.add_custom_button("View General Ledger", function() {
frappe.route_options = {
"voucher_no": frm.doc.name,
"from_date": frm.doc.available_for_use_date,
@@ -107,27 +129,9 @@ frappe.ui.form.on('Asset', {
"company": frm.doc.company
};
frappe.set_route("query-report", "General Ledger");
- });
+ }, __("Manage"));
}
- if (frm.doc.maintenance_required && !frm.doc.maintenance_schedule) {
- frm.add_custom_button(__("Asset Maintenance"), function() {
- frm.trigger("create_asset_maintenance");
- }, __('Create'));
- }
- if (frm.doc.status != 'Fully Depreciated') {
- frm.add_custom_button(__("Asset Value Adjustment"), function() {
- frm.trigger("create_asset_adjustment");
- }, __('Create'));
- }
-
- if (!frm.doc.calculate_depreciation) {
- frm.add_custom_button(__("Depreciation Entry"), function() {
- frm.trigger("make_journal_entry");
- }, __('Create'));
- }
-
- frm.page.set_inner_btn_group_as_primary(__('Create'));
frm.trigger("setup_chart");
}
@@ -304,6 +308,20 @@ frappe.ui.form.on('Asset', {
})
},
+ create_asset_repair: function(frm) {
+ frappe.call({
+ args: {
+ "asset": frm.doc.name,
+ "asset_name": frm.doc.asset_name
+ },
+ method: "erpnext.assets.doctype.asset.asset.create_asset_repair",
+ callback: function(r) {
+ var doclist = frappe.model.sync(r.message);
+ frappe.set_route("Form", doclist[0].doctype, doclist[0].name);
+ }
+ });
+ },
+
create_asset_adjustment: function(frm) {
frappe.call({
args: {
diff --git a/erpnext/assets/doctype/asset/asset.json b/erpnext/assets/doctype/asset/asset.json
index 421b9a6c37..de060757e2 100644
--- a/erpnext/assets/doctype/asset/asset.json
+++ b/erpnext/assets/doctype/asset/asset.json
@@ -502,7 +502,7 @@
"link_fieldname": "asset"
}
],
- "modified": "2021-01-22 12:38:59.091510",
+ "modified": "2021-06-24 14:58:51.097908",
"modified_by": "Administrator",
"module": "Assets",
"name": "Asset",
diff --git a/erpnext/assets/doctype/asset/asset.py b/erpnext/assets/doctype/asset/asset.py
index 8799275fc4..66f0bdcd58 100644
--- a/erpnext/assets/doctype/asset/asset.py
+++ b/erpnext/assets/doctype/asset/asset.py
@@ -168,17 +168,24 @@ class Asset(AccountsController):
d.precision("rate_of_depreciation"))
def make_depreciation_schedule(self):
- if 'Manual' not in [d.depreciation_method for d in self.finance_books]:
+ if 'Manual' not in [d.depreciation_method for d in self.finance_books] and not self.schedules:
self.schedules = []
- if self.get("schedules") or not self.available_for_use_date:
+ if not self.available_for_use_date:
return
for d in self.get('finance_books'):
self.validate_asset_finance_books(d)
+
+ start = self.clear_depreciation_schedule()
- value_after_depreciation = (flt(self.gross_purchase_amount) -
- flt(self.opening_accumulated_depreciation))
+ # value_after_depreciation - current Asset value
+ if d.value_after_depreciation:
+ value_after_depreciation = (flt(d.value_after_depreciation) -
+ flt(self.opening_accumulated_depreciation))
+ else:
+ value_after_depreciation = (flt(self.gross_purchase_amount) -
+ flt(self.opening_accumulated_depreciation))
d.value_after_depreciation = value_after_depreciation
@@ -191,7 +198,7 @@ class Asset(AccountsController):
number_of_pending_depreciations += 1
skip_row = False
- for n in range(number_of_pending_depreciations):
+ for n in range(start, number_of_pending_depreciations):
# If depreciation is already completed (for double declining balance)
if skip_row: continue
@@ -216,11 +223,13 @@ class Asset(AccountsController):
# For last row
elif has_pro_rata and n == cint(number_of_pending_depreciations) - 1:
- to_date = add_months(self.available_for_use_date,
- n * cint(d.frequency_of_depreciation))
+ if not self.flags.increase_in_asset_life:
+ # In case of increase_in_asset_life, the self.to_date is already set on asset_repair submission
+ self.to_date = add_months(self.available_for_use_date,
+ n * cint(d.frequency_of_depreciation))
depreciation_amount, days, months = self.get_pro_rata_amt(d,
- depreciation_amount, schedule_date, to_date)
+ depreciation_amount, schedule_date, self.to_date)
monthly_schedule_date = add_months(schedule_date, 1)
@@ -284,10 +293,23 @@ class Asset(AccountsController):
"finance_book_id": d.idx
})
+ # used when depreciation schedule needs to be modified due to increase in asset life
+ def clear_depreciation_schedule(self):
+ start = 0
+ for n in range(len(self.schedules)):
+ if not self.schedules[n].journal_entry:
+ del self.schedules[n:]
+ start = n
+ break
+ return start
+
+
+ # if it returns True, depreciation_amount will not be equal for the first and last rows
def check_is_pro_rata(self, row):
has_pro_rata = False
-
days = date_diff(row.depreciation_start_date, self.available_for_use_date) + 1
+
+ # if frequency_of_depreciation is 12 months, total_days = 365
total_days = get_total_days(row.depreciation_start_date, row.frequency_of_depreciation)
if days < total_days:
@@ -346,11 +368,12 @@ class Asset(AccountsController):
if d.finance_book_id not in finance_books:
accumulated_depreciation = flt(self.opening_accumulated_depreciation)
value_after_depreciation = flt(self.get_value_after_depreciation(d.finance_book_id))
- finance_books.append(d.finance_book_id)
+ finance_books.append(int(d.finance_book_id))
depreciation_amount = flt(d.depreciation_amount, d.precision("depreciation_amount"))
value_after_depreciation -= flt(depreciation_amount)
+ # for the last row, if depreciation method = Straight Line
if straight_line_idx and i == max(straight_line_idx) - 1:
book = self.get('finance_books')[cint(d.finance_book_id) - 1]
depreciation_amount += flt(value_after_depreciation -
@@ -625,9 +648,18 @@ def create_asset_maintenance(asset, item_code, item_name, asset_category, compan
})
return asset_maintenance
+@frappe.whitelist()
+def create_asset_repair(asset, asset_name):
+ asset_repair = frappe.new_doc("Asset Repair")
+ asset_repair.update({
+ "asset": asset,
+ "asset_name": asset_name
+ })
+ return asset_repair
+
@frappe.whitelist()
def create_asset_adjustment(asset, asset_category, company):
- asset_maintenance = frappe.new_doc("Asset Value Adjustment")
+ asset_maintenance = frappe.get_doc("Asset Value Adjustment")
asset_maintenance.update({
"asset": asset,
"company": company,
@@ -757,8 +789,15 @@ def get_depreciation_amount(asset, depreciable_value, row):
depreciation_left = flt(row.total_number_of_depreciations) - flt(asset.number_of_depreciations_booked)
if row.depreciation_method in ("Straight Line", "Manual"):
- depreciation_amount = (flt(row.value_after_depreciation) -
- flt(row.expected_value_after_useful_life)) / depreciation_left
+ # if the Depreciation Schedule is being prepared for the first time
+ if not asset.flags.increase_in_asset_life:
+ depreciation_amount = (flt(row.value_after_depreciation) -
+ flt(row.expected_value_after_useful_life)) / depreciation_left
+
+ # if the Depreciation Schedule is being modified after Asset Repair
+ else:
+ depreciation_amount = (flt(row.value_after_depreciation) -
+ flt(row.expected_value_after_useful_life)) / (date_diff(asset.to_date, asset.available_for_use_date) / 365)
else:
depreciation_amount = flt(depreciable_value * (flt(row.rate_of_depreciation) / 100))
diff --git a/erpnext/assets/doctype/asset/test_asset.py b/erpnext/assets/doctype/asset/test_asset.py
index 8845f24d10..59fbe3b030 100644
--- a/erpnext/assets/doctype/asset/test_asset.py
+++ b/erpnext/assets/doctype/asset/test_asset.py
@@ -125,7 +125,6 @@ class TestAsset(unittest.TestCase):
"frequency_of_depreciation": 12,
"depreciation_start_date": "2030-12-31"
})
- asset.insert()
self.assertEqual(asset.status, "Draft")
asset.save()
expected_schedules = [
@@ -154,9 +153,8 @@ class TestAsset(unittest.TestCase):
"frequency_of_depreciation": 12,
"depreciation_start_date": '2030-12-31'
})
- asset.insert()
- self.assertEqual(asset.status, "Draft")
asset.save()
+ self.assertEqual(asset.status, "Draft")
expected_schedules = [
['2030-12-31', 66667.00, 66667.00],
@@ -185,7 +183,7 @@ class TestAsset(unittest.TestCase):
"frequency_of_depreciation": 12,
"depreciation_start_date": "2030-12-31"
})
- asset.insert()
+ asset.save()
self.assertEqual(asset.status, "Draft")
expected_schedules = [
@@ -216,7 +214,6 @@ class TestAsset(unittest.TestCase):
"depreciation_start_date": "2030-12-31"
})
- asset.insert()
asset.save()
expected_schedules = [
@@ -247,7 +244,6 @@ class TestAsset(unittest.TestCase):
"frequency_of_depreciation": 10,
"depreciation_start_date": "2020-12-31"
})
- asset.insert()
asset.submit()
asset.load_from_db()
self.assertEqual(asset.status, "Submitted")
@@ -350,7 +346,6 @@ class TestAsset(unittest.TestCase):
"frequency_of_depreciation": 10,
"depreciation_start_date": "2020-12-31"
})
- asset.insert()
asset.submit()
post_depreciation_entries(date="2021-01-01")
@@ -380,7 +375,6 @@ class TestAsset(unittest.TestCase):
"total_number_of_depreciations": 10,
"frequency_of_depreciation": 1
})
- asset.insert()
asset.submit()
post_depreciation_entries(date=add_months('2020-01-01', 4))
@@ -424,7 +418,6 @@ class TestAsset(unittest.TestCase):
"frequency_of_depreciation": 10,
"depreciation_start_date": "2020-12-31"
})
- asset.insert()
asset.submit()
post_depreciation_entries(date="2021-01-01")
@@ -468,7 +461,7 @@ class TestAsset(unittest.TestCase):
"total_number_of_depreciations": 3,
"frequency_of_depreciation": 10
})
- asset.insert()
+ asset.save()
accumulated_depreciation_after_full_schedule = \
max(d.accumulated_depreciation_amount for d in asset.get("schedules"))
@@ -699,7 +692,7 @@ def create_asset(**args):
"item_code": args.item_code or "Macbook Pro",
"company": args.company or"_Test Company",
"purchase_date": "2015-01-01",
- "calculate_depreciation": 0,
+ "calculate_depreciation": args.calculate_depreciation or 0,
"gross_purchase_amount": 100000,
"purchase_receipt_amount": 100000,
"expected_value_after_useful_life": 10000,
@@ -707,9 +700,16 @@ def create_asset(**args):
"available_for_use_date": "2020-06-06",
"location": "Test Location",
"asset_owner": "Company",
- "is_existing_asset": args.is_existing_asset or 0
+ "is_existing_asset": 1
})
+ if asset.calculate_depreciation:
+ asset.append("finance_books", {
+ "depreciation_method": "Straight Line",
+ "frequency_of_depreciation": 12,
+ "total_number_of_depreciations": 5
+ })
+
try:
asset.save()
except frappe.DuplicateEntryError:
diff --git a/erpnext/assets/doctype/asset_finance_book/asset_finance_book.json b/erpnext/assets/doctype/asset_finance_book/asset_finance_book.json
index d9b7b695f7..e5a5f194c1 100644
--- a/erpnext/assets/doctype/asset_finance_book/asset_finance_book.json
+++ b/erpnext/assets/doctype/asset_finance_book/asset_finance_book.json
@@ -67,7 +67,6 @@
{
"fieldname": "value_after_depreciation",
"fieldtype": "Currency",
- "hidden": 1,
"label": "Value After Depreciation",
"no_copy": 1,
"options": "Company:company:default_currency",
@@ -85,7 +84,7 @@
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
- "modified": "2020-11-05 16:30:09.213479",
+ "modified": "2021-06-17 12:59:05.743683",
"modified_by": "Administrator",
"module": "Assets",
"name": "Asset Finance Book",
diff --git a/erpnext/assets/doctype/asset_repair/asset_repair.js b/erpnext/assets/doctype/asset_repair/asset_repair.js
index 4ba2b4474a..1cebfff66e 100644
--- a/erpnext/assets/doctype/asset_repair/asset_repair.js
+++ b/erpnext/assets/doctype/asset_repair/asset_repair.js
@@ -2,6 +2,45 @@
// For license information, please see license.txt
frappe.ui.form.on('Asset Repair', {
+ setup: function(frm) {
+ frm.fields_dict.cost_center.get_query = function(doc) {
+ return {
+ filters: {
+ 'is_group': 0,
+ 'company': doc.company
+ }
+ };
+ };
+
+ frm.fields_dict.project.get_query = function(doc) {
+ return {
+ filters: {
+ 'company': doc.company
+ }
+ };
+ };
+
+ frm.fields_dict.warehouse.get_query = function(doc) {
+ return {
+ filters: {
+ 'is_group': 0,
+ 'company': doc.company
+ }
+ };
+ };
+ },
+
+ refresh: function(frm) {
+ if (frm.doc.docstatus) {
+ frm.add_custom_button("View General Ledger", function() {
+ frappe.route_options = {
+ "voucher_no": frm.doc.name
+ };
+ frappe.set_route("query-report", "General Ledger");
+ });
+ }
+ },
+
repair_status: (frm) => {
if (frm.doc.completion_date && frm.doc.repair_status == "Completed") {
frappe.call ({
@@ -17,5 +56,16 @@ frappe.ui.form.on('Asset Repair', {
}
});
}
+
+ if (frm.doc.repair_status == "Completed") {
+ frm.set_value('completion_date', frappe.datetime.now_datetime());
+ }
}
});
+
+frappe.ui.form.on('Asset Repair Consumed Item', {
+ consumed_quantity: function(frm, cdt, cdn) {
+ var row = locals[cdt][cdn];
+ frappe.model.set_value(cdt, cdn, 'total_value', row.consumed_quantity * row.valuation_rate);
+ },
+});
\ No newline at end of file
diff --git a/erpnext/assets/doctype/asset_repair/asset_repair.json b/erpnext/assets/doctype/asset_repair/asset_repair.json
index d338fc0fb7..ba3189887c 100644
--- a/erpnext/assets/doctype/asset_repair/asset_repair.json
+++ b/erpnext/assets/doctype/asset_repair/asset_repair.json
@@ -7,38 +7,43 @@
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
- "naming_series",
- "asset_name",
+ "asset",
+ "company",
"column_break_2",
- "item_code",
- "item_name",
+ "asset_name",
+ "naming_series",
"section_break_5",
"failure_date",
- "assign_to",
- "assign_to_name",
+ "repair_status",
"column_break_6",
"completion_date",
- "repair_status",
+ "accounting_dimensions_section",
+ "cost_center",
+ "column_break_14",
+ "project",
+ "accounting_details",
"repair_cost",
+ "capitalize_repair_cost",
+ "stock_consumption",
+ "column_break_8",
+ "purchase_invoice",
+ "stock_consumption_details_section",
+ "warehouse",
+ "stock_items",
+ "total_repair_cost",
+ "stock_entry",
+ "asset_depreciation_details_section",
+ "increase_in_asset_life",
"section_break_9",
"description",
"column_break_9",
"actions_performed",
- "section_break_17",
+ "section_break_23",
"downtime",
"column_break_19",
"amended_from"
],
"fields": [
- {
- "columns": 1,
- "fieldname": "asset_name",
- "fieldtype": "Link",
- "in_list_view": 1,
- "label": "Asset",
- "options": "Asset",
- "reqd": 1
- },
{
"fieldname": "naming_series",
"fieldtype": "Select",
@@ -50,18 +55,6 @@
"fieldname": "column_break_2",
"fieldtype": "Column Break"
},
- {
- "fetch_from": "asset_name.item_code",
- "fieldname": "item_code",
- "fieldtype": "Read Only",
- "label": "Item Code"
- },
- {
- "fetch_from": "asset_name.item_name",
- "fieldname": "item_name",
- "fieldtype": "Read Only",
- "label": "Item Name"
- },
{
"fieldname": "section_break_5",
"fieldtype": "Section Break",
@@ -74,33 +67,20 @@
"label": "Failure Date",
"reqd": 1
},
- {
- "allow_on_submit": 1,
- "fieldname": "assign_to",
- "fieldtype": "Link",
- "label": "Assign To",
- "options": "User"
- },
- {
- "allow_on_submit": 1,
- "fetch_from": "assign_to.full_name",
- "fieldname": "assign_to_name",
- "fieldtype": "Read Only",
- "label": "Assign To Name"
- },
{
"fieldname": "column_break_6",
"fieldtype": "Column Break"
},
{
- "allow_on_submit": 1,
+ "depends_on": "eval:!doc.__islocal",
"fieldname": "completion_date",
"fieldtype": "Datetime",
- "label": "Completion Date"
+ "label": "Completion Date",
+ "no_copy": 1
},
{
- "allow_on_submit": 1,
"default": "Pending",
+ "depends_on": "eval:!doc.__islocal",
"fieldname": "repair_status",
"fieldtype": "Select",
"label": "Repair Status",
@@ -116,25 +96,18 @@
{
"fieldname": "description",
"fieldtype": "Long Text",
- "label": "Error Description",
- "reqd": 1
+ "label": "Error Description"
},
{
"fieldname": "column_break_9",
"fieldtype": "Column Break"
},
{
- "allow_on_submit": 1,
"fieldname": "actions_performed",
"fieldtype": "Long Text",
"label": "Actions performed"
},
{
- "fieldname": "section_break_17",
- "fieldtype": "Section Break"
- },
- {
- "allow_on_submit": 1,
"fieldname": "downtime",
"fieldtype": "Data",
"in_list_view": 1,
@@ -146,7 +119,7 @@
"fieldtype": "Column Break"
},
{
- "allow_on_submit": 1,
+ "default": "0",
"fieldname": "repair_cost",
"fieldtype": "Currency",
"label": "Repair Cost"
@@ -159,12 +132,139 @@
"options": "Asset Repair",
"print_hide": 1,
"read_only": 1
+ },
+ {
+ "columns": 1,
+ "fieldname": "asset",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "label": "Asset",
+ "options": "Asset",
+ "reqd": 1
+ },
+ {
+ "fetch_from": "asset.asset_name",
+ "fieldname": "asset_name",
+ "fieldtype": "Read Only",
+ "label": "Asset Name"
+ },
+ {
+ "fieldname": "column_break_8",
+ "fieldtype": "Column Break"
+ },
+ {
+ "default": "0",
+ "depends_on": "eval:!doc.__islocal",
+ "fieldname": "capitalize_repair_cost",
+ "fieldtype": "Check",
+ "label": "Capitalize Repair Cost"
+ },
+ {
+ "fieldname": "accounting_details",
+ "fieldtype": "Section Break",
+ "label": "Accounting Details"
+ },
+ {
+ "fieldname": "stock_items",
+ "fieldtype": "Table",
+ "label": "Stock Items",
+ "mandatory_depends_on": "stock_consumption",
+ "options": "Asset Repair Consumed Item"
+ },
+ {
+ "fieldname": "section_break_23",
+ "fieldtype": "Section Break"
+ },
+ {
+ "collapsible": 1,
+ "fieldname": "accounting_dimensions_section",
+ "fieldtype": "Section Break",
+ "label": "Accounting Dimensions"
+ },
+ {
+ "fieldname": "cost_center",
+ "fieldtype": "Link",
+ "label": "Cost Center",
+ "options": "Cost Center"
+ },
+ {
+ "fieldname": "project",
+ "fieldtype": "Link",
+ "label": "Project",
+ "options": "Project"
+ },
+ {
+ "fieldname": "column_break_14",
+ "fieldtype": "Column Break"
+ },
+ {
+ "default": "0",
+ "depends_on": "eval:!doc.__islocal",
+ "fieldname": "stock_consumption",
+ "fieldtype": "Check",
+ "label": "Stock Consumed During Repair"
+ },
+ {
+ "depends_on": "stock_consumption",
+ "fieldname": "stock_consumption_details_section",
+ "fieldtype": "Section Break",
+ "label": "Stock Consumption Details"
+ },
+ {
+ "depends_on": "eval: doc.stock_consumption && doc.total_repair_cost > 0",
+ "description": "Sum of Repair Cost and Value of Consumed Stock Items.",
+ "fieldname": "total_repair_cost",
+ "fieldtype": "Currency",
+ "label": "Total Repair Cost",
+ "read_only": 1
+ },
+ {
+ "depends_on": "stock_consumption",
+ "fieldname": "warehouse",
+ "fieldtype": "Link",
+ "label": "Warehouse",
+ "options": "Warehouse"
+ },
+ {
+ "depends_on": "capitalize_repair_cost",
+ "fieldname": "asset_depreciation_details_section",
+ "fieldtype": "Section Break",
+ "label": "Asset Depreciation Details"
+ },
+ {
+ "fieldname": "increase_in_asset_life",
+ "fieldtype": "Int",
+ "label": "Increase In Asset Life(Months)",
+ "no_copy": 1
+ },
+ {
+ "depends_on": "eval:!doc.__islocal",
+ "fieldname": "purchase_invoice",
+ "fieldtype": "Link",
+ "label": "Purchase Invoice",
+ "mandatory_depends_on": "eval: doc.repair_status == 'Completed' && doc.repair_cost > 0",
+ "no_copy": 1,
+ "options": "Purchase Invoice"
+ },
+ {
+ "fetch_from": "asset.company",
+ "fieldname": "company",
+ "fieldtype": "Link",
+ "label": "Company",
+ "options": "Company"
+ },
+ {
+ "fieldname": "stock_entry",
+ "fieldtype": "Link",
+ "label": "Stock Entry",
+ "options": "Stock Entry",
+ "read_only": 1
}
],
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
- "modified": "2021-01-22 15:08:12.495850",
+ "modified": "2021-06-25 13:14:38.307723",
"modified_by": "Administrator",
"module": "Assets",
"name": "Asset Repair",
@@ -203,6 +303,7 @@
],
"sort_field": "modified",
"sort_order": "DESC",
+ "title_field": "asset_name",
"track_changes": 1,
"track_seen": 1
}
\ No newline at end of file
diff --git a/erpnext/assets/doctype/asset_repair/asset_repair.py b/erpnext/assets/doctype/asset_repair/asset_repair.py
index 049b931b5e..d32fdf7054 100644
--- a/erpnext/assets/doctype/asset_repair/asset_repair.py
+++ b/erpnext/assets/doctype/asset_repair/asset_repair.py
@@ -5,16 +5,252 @@
from __future__ import unicode_literals
import frappe
from frappe import _
-from frappe.utils import time_diff_in_hours
-from frappe.model.document import Document
+from frappe.utils import time_diff_in_hours, getdate, add_months, flt, cint
+from erpnext.accounts.general_ledger import make_gl_entries
+from erpnext.assets.doctype.asset.asset import get_asset_account
+from erpnext.controllers.accounts_controller import AccountsController
-class AssetRepair(Document):
+class AssetRepair(AccountsController):
def validate(self):
- if self.repair_status == "Completed" and not self.completion_date:
- frappe.throw(_("Please select Completion Date for Completed Repair"))
+ self.asset_doc = frappe.get_doc('Asset', self.asset)
+ self.update_status()
+ if self.get('stock_items'):
+ self.set_total_value()
+ self.calculate_total_repair_cost()
+
+ def update_status(self):
+ if self.repair_status == 'Pending':
+ frappe.db.set_value('Asset', self.asset, 'status', 'Out of Order')
+ else:
+ self.asset_doc.set_status()
+
+ def set_total_value(self):
+ for item in self.get('stock_items'):
+ item.total_value = flt(item.valuation_rate) * flt(item.consumed_quantity)
+
+ def calculate_total_repair_cost(self):
+ self.total_repair_cost = flt(self.repair_cost)
+
+ total_value_of_stock_consumed = self.get_total_value_of_stock_consumed()
+ self.total_repair_cost += total_value_of_stock_consumed
+
+ def before_submit(self):
+ self.check_repair_status()
+
+ if self.get('stock_consumption') or self.get('capitalize_repair_cost'):
+ self.increase_asset_value()
+ if self.get('stock_consumption'):
+ self.check_for_stock_items_and_warehouse()
+ self.decrease_stock_quantity()
+ if self.get('capitalize_repair_cost'):
+ self.make_gl_entries()
+ if frappe.db.get_value('Asset', self.asset, 'calculate_depreciation') and self.increase_in_asset_life:
+ self.modify_depreciation_schedule()
+
+ self.asset_doc.flags.ignore_validate_update_after_submit = True
+ self.asset_doc.prepare_depreciation_data()
+ self.asset_doc.save()
+
+ def before_cancel(self):
+ self.asset_doc = frappe.get_doc('Asset', self.asset)
+
+ if self.get('stock_consumption') or self.get('capitalize_repair_cost'):
+ self.decrease_asset_value()
+ if self.get('stock_consumption'):
+ self.increase_stock_quantity()
+ if self.get('capitalize_repair_cost'):
+ self.ignore_linked_doctypes = ('GL Entry', 'Stock Ledger Entry')
+ self.make_gl_entries(cancel=True)
+ if frappe.db.get_value('Asset', self.asset, 'calculate_depreciation') and self.increase_in_asset_life:
+ self.revert_depreciation_schedule_on_cancellation()
+
+ self.asset_doc.flags.ignore_validate_update_after_submit = True
+ self.asset_doc.prepare_depreciation_data()
+ self.asset_doc.save()
+
+ def check_repair_status(self):
+ if self.repair_status == "Pending":
+ frappe.throw(_("Please update Repair Status."))
+
+ def check_for_stock_items_and_warehouse(self):
+ if not self.get('stock_items'):
+ frappe.throw(_("Please enter Stock Items consumed during the Repair."), title=_("Missing Items"))
+ if not self.warehouse:
+ frappe.throw(_("Please enter Warehouse from which Stock Items consumed during the Repair were taken."), title=_("Missing Warehouse"))
+
+ def increase_asset_value(self):
+ total_value_of_stock_consumed = self.get_total_value_of_stock_consumed()
+
+ if self.asset_doc.calculate_depreciation:
+ for row in self.asset_doc.finance_books:
+ row.value_after_depreciation += total_value_of_stock_consumed
+
+ if self.capitalize_repair_cost:
+ row.value_after_depreciation += self.repair_cost
+
+ def decrease_asset_value(self):
+ total_value_of_stock_consumed = self.get_total_value_of_stock_consumed()
+
+ if self.asset_doc.calculate_depreciation:
+ for row in self.asset_doc.finance_books:
+ row.value_after_depreciation -= total_value_of_stock_consumed
+
+ if self.capitalize_repair_cost:
+ row.value_after_depreciation -= self.repair_cost
+
+ def get_total_value_of_stock_consumed(self):
+ total_value_of_stock_consumed = 0
+ if self.get('stock_consumption'):
+ for item in self.get('stock_items'):
+ total_value_of_stock_consumed += item.total_value
+
+ return total_value_of_stock_consumed
+
+ def decrease_stock_quantity(self):
+ stock_entry = frappe.get_doc({
+ "doctype": "Stock Entry",
+ "stock_entry_type": "Material Issue",
+ "company": self.company
+ })
+
+ for stock_item in self.get('stock_items'):
+ stock_entry.append('items', {
+ "s_warehouse": self.warehouse,
+ "item_code": stock_item.item,
+ "qty": stock_item.consumed_quantity,
+ "basic_rate": stock_item.valuation_rate
+ })
+
+ stock_entry.insert()
+ stock_entry.submit()
+
+ self.db_set('stock_entry', stock_entry.name)
+
+ def increase_stock_quantity(self):
+ stock_entry = frappe.get_doc('Stock Entry', self.stock_entry)
+ stock_entry.flags.ignore_links = True
+ stock_entry.cancel()
+
+ def make_gl_entries(self, cancel=False):
+ if flt(self.repair_cost) > 0:
+ gl_entries = self.get_gl_entries()
+ make_gl_entries(gl_entries, cancel)
+
+ def get_gl_entries(self):
+ gl_entries = []
+ repair_and_maintenance_account = frappe.db.get_value('Company', self.company, 'repair_and_maintenance_account')
+ fixed_asset_account = get_asset_account("fixed_asset_account", asset=self.asset, company=self.company)
+ expense_account = frappe.get_doc('Purchase Invoice', self.purchase_invoice).items[0].expense_account
+
+ gl_entries.append(
+ self.get_gl_dict({
+ "account": expense_account,
+ "credit": self.repair_cost,
+ "credit_in_account_currency": self.repair_cost,
+ "against": repair_and_maintenance_account,
+ "voucher_type": self.doctype,
+ "voucher_no": self.name,
+ "cost_center": self.cost_center,
+ "posting_date": getdate(),
+ "company": self.company
+ }, item=self)
+ )
+
+ if self.get('stock_consumption'):
+ # creating GL Entries for each row in Stock Items based on the Stock Entry created for it
+ stock_entry = frappe.get_doc('Stock Entry', self.stock_entry)
+ for item in stock_entry.items:
+ gl_entries.append(
+ self.get_gl_dict({
+ "account": item.expense_account,
+ "credit": item.amount,
+ "credit_in_account_currency": item.amount,
+ "against": repair_and_maintenance_account,
+ "voucher_type": self.doctype,
+ "voucher_no": self.name,
+ "cost_center": self.cost_center,
+ "posting_date": getdate(),
+ "company": self.company
+ }, item=self)
+ )
+
+ gl_entries.append(
+ self.get_gl_dict({
+ "account": fixed_asset_account,
+ "debit": self.total_repair_cost,
+ "debit_in_account_currency": self.total_repair_cost,
+ "against": expense_account,
+ "voucher_type": self.doctype,
+ "voucher_no": self.name,
+ "cost_center": self.cost_center,
+ "posting_date": getdate(),
+ "against_voucher_type": "Purchase Invoice",
+ "against_voucher": self.purchase_invoice,
+ "company": self.company
+ }, item=self)
+ )
+
+ return gl_entries
+
+ def modify_depreciation_schedule(self):
+ for row in self.asset_doc.finance_books:
+ row.total_number_of_depreciations += self.increase_in_asset_life/row.frequency_of_depreciation
+
+ self.asset_doc.flags.increase_in_asset_life = False
+ extra_months = self.increase_in_asset_life % row.frequency_of_depreciation
+ if extra_months != 0:
+ self.calculate_last_schedule_date(self.asset_doc, row, extra_months)
+
+ # to help modify depreciation schedule when increase_in_asset_life is not a multiple of frequency_of_depreciation
+ def calculate_last_schedule_date(self, asset, row, extra_months):
+ asset.flags.increase_in_asset_life = True
+ number_of_pending_depreciations = cint(row.total_number_of_depreciations) - \
+ cint(asset.number_of_depreciations_booked)
+
+ # the Schedule Date in the final row of the old Depreciation Schedule
+ last_schedule_date = asset.schedules[len(asset.schedules)-1].schedule_date
+
+ # the Schedule Date in the final row of the new Depreciation Schedule
+ asset.to_date = add_months(last_schedule_date, extra_months)
+
+ # the latest possible date at which the depreciation can occur, without increasing the Total Number of Depreciations
+ # if depreciations happen yearly and the Depreciation Posting Date is 01-01-2020, this could be 01-01-2021, 01-01-2022...
+ schedule_date = add_months(row.depreciation_start_date,
+ number_of_pending_depreciations * cint(row.frequency_of_depreciation))
+
+ if asset.to_date > schedule_date:
+ row.total_number_of_depreciations += 1
+
+ def revert_depreciation_schedule_on_cancellation(self):
+ for row in self.asset_doc.finance_books:
+ row.total_number_of_depreciations -= self.increase_in_asset_life/row.frequency_of_depreciation
+
+ self.asset_doc.flags.increase_in_asset_life = False
+ extra_months = self.increase_in_asset_life % row.frequency_of_depreciation
+ if extra_months != 0:
+ self.calculate_last_schedule_date_before_modification(self.asset_doc, row, extra_months)
+
+ def calculate_last_schedule_date_before_modification(self, asset, row, extra_months):
+ asset.flags.increase_in_asset_life = True
+ number_of_pending_depreciations = cint(row.total_number_of_depreciations) - \
+ cint(asset.number_of_depreciations_booked)
+
+ # the Schedule Date in the final row of the modified Depreciation Schedule
+ last_schedule_date = asset.schedules[len(asset.schedules)-1].schedule_date
+
+ # the Schedule Date in the final row of the original Depreciation Schedule
+ asset.to_date = add_months(last_schedule_date, -extra_months)
+
+ # the latest possible date at which the depreciation can occur, without decreasing the Total Number of Depreciations
+ # if depreciations happen yearly and the Depreciation Posting Date is 01-01-2020, this could be 01-01-2021, 01-01-2022...
+ schedule_date = add_months(row.depreciation_start_date,
+ (number_of_pending_depreciations - 1) * cint(row.frequency_of_depreciation))
+
+ if asset.to_date < schedule_date:
+ row.total_number_of_depreciations -= 1
@frappe.whitelist()
def get_downtime(failure_date, completion_date):
downtime = time_diff_in_hours(completion_date, failure_date)
- return round(downtime, 2)
\ No newline at end of file
+ return round(downtime, 2)
diff --git a/erpnext/assets/doctype/asset_repair/test_asset_repair.py b/erpnext/assets/doctype/asset_repair/test_asset_repair.py
index 3d325a9683..30bbb37851 100644
--- a/erpnext/assets/doctype/asset_repair/test_asset_repair.py
+++ b/erpnext/assets/doctype/asset_repair/test_asset_repair.py
@@ -2,8 +2,167 @@
# Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
from __future__ import unicode_literals
-
+import frappe
+from frappe.utils import nowdate, flt
import unittest
+from erpnext.assets.doctype.asset.test_asset import create_asset_data, create_asset, set_depreciation_settings_in_company
class TestAssetRepair(unittest.TestCase):
- pass
+ def setUp(self):
+ set_depreciation_settings_in_company()
+ create_asset_data()
+ frappe.db.sql("delete from `tabTax Rule`")
+
+ def test_update_status(self):
+ asset = create_asset()
+ initial_status = asset.status
+ asset_repair = create_asset_repair(asset = asset)
+
+ if asset_repair.repair_status == "Pending":
+ asset.reload()
+ self.assertEqual(asset.status, "Out of Order")
+
+ asset_repair.repair_status = "Completed"
+ asset_repair.save()
+ asset_status = frappe.db.get_value("Asset", asset_repair.asset, "status")
+ self.assertEqual(asset_status, initial_status)
+
+ def test_stock_item_total_value(self):
+ asset_repair = create_asset_repair(stock_consumption = 1)
+
+ for item in asset_repair.stock_items:
+ total_value = flt(item.valuation_rate) * flt(item.consumed_quantity)
+ self.assertEqual(item.total_value, total_value)
+
+ def test_total_repair_cost(self):
+ asset_repair = create_asset_repair(stock_consumption = 1)
+
+ total_repair_cost = asset_repair.repair_cost
+ self.assertEqual(total_repair_cost, asset_repair.repair_cost)
+ for item in asset_repair.stock_items:
+ total_repair_cost += item.total_value
+
+ self.assertEqual(total_repair_cost, asset_repair.total_repair_cost)
+
+ def test_repair_status_after_submit(self):
+ asset_repair = create_asset_repair(submit = 1)
+ self.assertNotEqual(asset_repair.repair_status, "Pending")
+
+ def test_stock_items(self):
+ asset_repair = create_asset_repair(stock_consumption = 1)
+ self.assertTrue(asset_repair.stock_consumption)
+ self.assertTrue(asset_repair.stock_items)
+
+ def test_warehouse(self):
+ asset_repair = create_asset_repair(stock_consumption = 1)
+ self.assertTrue(asset_repair.stock_consumption)
+ self.assertTrue(asset_repair.warehouse)
+
+ def test_decrease_stock_quantity(self):
+ asset_repair = create_asset_repair(stock_consumption = 1, submit = 1)
+ stock_entry = frappe.get_last_doc('Stock Entry')
+
+ self.assertEqual(stock_entry.stock_entry_type, "Material Issue")
+ self.assertEqual(stock_entry.items[0].s_warehouse, asset_repair.warehouse)
+ self.assertEqual(stock_entry.items[0].item_code, asset_repair.stock_items[0].item)
+ self.assertEqual(stock_entry.items[0].qty, asset_repair.stock_items[0].consumed_quantity)
+
+ def test_increase_in_asset_value_due_to_stock_consumption(self):
+ asset = create_asset(calculate_depreciation = 1)
+ initial_asset_value = get_asset_value(asset)
+ asset_repair = create_asset_repair(asset= asset, stock_consumption = 1, submit = 1)
+ asset.reload()
+
+ increase_in_asset_value = get_asset_value(asset) - initial_asset_value
+ self.assertEqual(asset_repair.stock_items[0].total_value, increase_in_asset_value)
+
+ def test_increase_in_asset_value_due_to_repair_cost_capitalisation(self):
+ asset = create_asset(calculate_depreciation = 1)
+ initial_asset_value = get_asset_value(asset)
+ asset_repair = create_asset_repair(asset= asset, capitalize_repair_cost = 1, submit = 1)
+ asset.reload()
+
+ increase_in_asset_value = get_asset_value(asset) - initial_asset_value
+ self.assertEqual(asset_repair.repair_cost, increase_in_asset_value)
+
+ def test_purchase_invoice(self):
+ asset_repair = create_asset_repair(capitalize_repair_cost = 1, submit = 1)
+ self.assertTrue(asset_repair.purchase_invoice)
+
+ def test_gl_entries(self):
+ asset_repair = create_asset_repair(capitalize_repair_cost = 1, submit = 1)
+ gl_entry = frappe.get_last_doc('GL Entry')
+ self.assertEqual(asset_repair.name, gl_entry.voucher_no)
+
+ def test_increase_in_asset_life(self):
+ asset = create_asset(calculate_depreciation = 1)
+ initial_num_of_depreciations = num_of_depreciations(asset)
+ create_asset_repair(asset= asset, capitalize_repair_cost = 1, submit = 1)
+ asset.reload()
+
+ self.assertEqual((initial_num_of_depreciations + 1), num_of_depreciations(asset))
+ self.assertEqual(asset.schedules[-1].accumulated_depreciation_amount, asset.finance_books[0].value_after_depreciation)
+
+def get_asset_value(asset):
+ return asset.finance_books[0].value_after_depreciation
+
+def num_of_depreciations(asset):
+ return asset.finance_books[0].total_number_of_depreciations
+
+def create_asset_repair(**args):
+ from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse
+ from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice
+
+ args = frappe._dict(args)
+
+ if args.asset:
+ asset = args.asset
+ else:
+ asset = create_asset(is_existing_asset = 1)
+ asset_repair = frappe.new_doc("Asset Repair")
+ asset_repair.update({
+ "asset": asset.name,
+ "asset_name": asset.asset_name,
+ "failure_date": nowdate(),
+ "description": "Test Description",
+ "repair_cost": 0,
+ "company": asset.company
+ })
+
+ if args.stock_consumption:
+ asset_repair.stock_consumption = 1
+ asset_repair.warehouse = create_warehouse("Test Warehouse", company = asset.company)
+ asset_repair.append("stock_items", {
+ "item": args.item or args.item_code or "_Test Item",
+ "valuation_rate": args.rate if args.get("rate") is not None else 100,
+ "consumed_quantity": args.qty or 1
+ })
+
+ asset_repair.insert(ignore_if_duplicate=True)
+
+ if args.submit:
+ asset_repair.repair_status = "Completed"
+ asset_repair.cost_center = "_Test Cost Center - _TC"
+
+ if args.stock_consumption:
+ stock_entry = frappe.get_doc({
+ "doctype": "Stock Entry",
+ "stock_entry_type": "Material Receipt",
+ "company": asset.company
+ })
+ stock_entry.append('items', {
+ "t_warehouse": asset_repair.warehouse,
+ "item_code": asset_repair.stock_items[0].item,
+ "qty": asset_repair.stock_items[0].consumed_quantity
+ })
+ stock_entry.submit()
+
+ if args.capitalize_repair_cost:
+ asset_repair.capitalize_repair_cost = 1
+ asset_repair.repair_cost = 1000
+ if asset.calculate_depreciation:
+ asset_repair.increase_in_asset_life = 12
+ asset_repair.purchase_invoice = make_purchase_invoice().name
+
+ asset_repair.submit()
+ return asset_repair
\ No newline at end of file
diff --git a/erpnext/assets/doctype/asset_repair_consumed_item/__init__.py b/erpnext/assets/doctype/asset_repair_consumed_item/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/erpnext/assets/doctype/asset_repair_consumed_item/asset_repair_consumed_item.json b/erpnext/assets/doctype/asset_repair_consumed_item/asset_repair_consumed_item.json
new file mode 100644
index 0000000000..528f0ec986
--- /dev/null
+++ b/erpnext/assets/doctype/asset_repair_consumed_item/asset_repair_consumed_item.json
@@ -0,0 +1,55 @@
+{
+ "actions": [],
+ "creation": "2021-05-12 02:41:54.161024",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "item",
+ "valuation_rate",
+ "consumed_quantity",
+ "total_value"
+ ],
+ "fields": [
+ {
+ "fieldname": "item",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "label": "Item",
+ "options": "Item"
+ },
+ {
+ "fetch_from": "item.valuation_rate",
+ "fieldname": "valuation_rate",
+ "fieldtype": "Currency",
+ "in_list_view": 1,
+ "label": "Valuation Rate",
+ "read_only": 1
+ },
+ {
+ "fieldname": "consumed_quantity",
+ "fieldtype": "Data",
+ "in_list_view": 1,
+ "label": "Consumed Quantity"
+ },
+ {
+ "fieldname": "total_value",
+ "fieldtype": "Currency",
+ "in_list_view": 1,
+ "label": "Total Value",
+ "read_only": 1
+ }
+ ],
+ "index_web_pages_for_search": 1,
+ "istable": 1,
+ "links": [],
+ "modified": "2021-05-12 03:19:55.006300",
+ "modified_by": "Administrator",
+ "module": "Assets",
+ "name": "Asset Repair Consumed Item",
+ "owner": "Administrator",
+ "permissions": [],
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "track_changes": 1
+}
\ No newline at end of file
diff --git a/erpnext/assets/doctype/asset_repair_consumed_item/asset_repair_consumed_item.py b/erpnext/assets/doctype/asset_repair_consumed_item/asset_repair_consumed_item.py
new file mode 100644
index 0000000000..fa22a5712f
--- /dev/null
+++ b/erpnext/assets/doctype/asset_repair_consumed_item/asset_repair_consumed_item.py
@@ -0,0 +1,8 @@
+# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+# import frappe
+from frappe.model.document import Document
+
+class AssetRepairConsumedItem(Document):
+ pass
diff --git a/erpnext/buying/doctype/buying_settings/buying_settings.json b/erpnext/buying/doctype/buying_settings/buying_settings.json
index 838a9abf8c..b9c77d59b1 100644
--- a/erpnext/buying/doctype/buying_settings/buying_settings.json
+++ b/erpnext/buying/doctype/buying_settings/buying_settings.json
@@ -123,7 +123,7 @@
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
- "modified": "2021-06-23 19:40:00.120822",
+ "modified": "2021-06-24 10:38:28.934525",
"modified_by": "Administrator",
"module": "Buying",
"name": "Buying Settings",
diff --git a/erpnext/change_log/v13/v13_6_0.md b/erpnext/change_log/v13/v13_6_0.md
new file mode 100644
index 0000000000..d881b279e3
--- /dev/null
+++ b/erpnext/change_log/v13/v13_6_0.md
@@ -0,0 +1,72 @@
+# Version 13.6.0 Release Notes
+
+### Features & Enhancements
+
+- Job Card Enhancements ([#24523](https://github.com/frappe/erpnext/pull/24523))
+- Implement multi-account selection in General Ledger([#26044](https://github.com/frappe/erpnext/pull/26044))
+- Fetching of qty as per received qty from PR to PI ([#26184](https://github.com/frappe/erpnext/pull/26184))
+- Subcontract code refactor and enhancement ([#25878](https://github.com/frappe/erpnext/pull/25878))
+- Employee Grievance ([#25705](https://github.com/frappe/erpnext/pull/25705))
+- Add Inactive status to Employee ([#26030](https://github.com/frappe/erpnext/pull/26030))
+- Incorrect valuation rate report for serialized items ([#25696](https://github.com/frappe/erpnext/pull/25696))
+- Update cost updates operation time and hour rates in BOM ([#25891](https://github.com/frappe/erpnext/pull/25891))
+
+### Fixes
+
+- Precision rate for packed items in internal transfers ([#26046](https://github.com/frappe/erpnext/pull/26046))
+- User is not able to change item tax template ([#26176](https://github.com/frappe/erpnext/pull/26176))
+- Insufficient permission for Dunning error ([#26092](https://github.com/frappe/erpnext/pull/26092))
+- Validate Product Bundle for existing transactions before deletion ([#25978](https://github.com/frappe/erpnext/pull/25978))
+- Auto unlink warehouse from item on delete ([#26073](https://github.com/frappe/erpnext/pull/26073))
+- Employee Inactive status implications ([#26245](https://github.com/frappe/erpnext/pull/26245))
+- Fetch batch items in stock reconciliation ([#26230](https://github.com/frappe/erpnext/pull/26230))
+- Disabled cancellation for sales order if linked to drafted sales invoice ([#26125](https://github.com/frappe/erpnext/pull/26125))
+- Sort website products by weightage mentioned in Item master ([#26134](https://github.com/frappe/erpnext/pull/26134))
+- Added freeze when trying to stop work order (#26192) ([#26196](https://github.com/frappe/erpnext/pull/26196))
+- Accounting Dimensions for payroll entry accrual Journal Entry ([#26083](https://github.com/frappe/erpnext/pull/26083))
+- Staffing plan vacancies data type issue ([#25941](https://github.com/frappe/erpnext/pull/25941))
+- Unable to enter score in Assessment Result details grid ([#25945](https://github.com/frappe/erpnext/pull/25945))
+- Report Subcontracted Raw Materials to be Transferred ([#26011](https://github.com/frappe/erpnext/pull/26011))
+- Label for enabling ledger posting of change amount ([#26070](https://github.com/frappe/erpnext/pull/26070))
+- Training event ([#26071](https://github.com/frappe/erpnext/pull/26071))
+- Rate not able to change in purchase order ([#26122](https://github.com/frappe/erpnext/pull/26122))
+- Error while fetching item taxes ([#26220](https://github.com/frappe/erpnext/pull/26220))
+- Check for duplicate payment terms in Payment Term Template ([#26003](https://github.com/frappe/erpnext/pull/26003))
+- Removed values out of sync validation from stock transactions ([#26229](https://github.com/frappe/erpnext/pull/26229))
+- Fetching employee in payroll entry ([#26269](https://github.com/frappe/erpnext/pull/26269))
+- Filter Cost Center and Project drop-down lists by Company ([#26045](https://github.com/frappe/erpnext/pull/26045))
+- Website item group logic for product listing in Item Group pages ([#26170](https://github.com/frappe/erpnext/pull/26170))
+- Chart not visible for First Response Time reports ([#26032](https://github.com/frappe/erpnext/pull/26032))
+- Incorrect billed qty in Sales Order analytics ([#26095](https://github.com/frappe/erpnext/pull/26095))
+- Material request and supplier quotation not linked if supplier quotation created from supplier portal ([#26023](https://github.com/frappe/erpnext/pull/26023))
+- Update leave allocation after submit ([#26191](https://github.com/frappe/erpnext/pull/26191))
+- Taxes on Internal Transfer payment entry ([#26188](https://github.com/frappe/erpnext/pull/26188))
+- Precision rate for packed items (bp #26046) ([#26217](https://github.com/frappe/erpnext/pull/26217))
+- Fixed rounding off ordered percent to 100 in condition ([#26152](https://github.com/frappe/erpnext/pull/26152))
+- Sanctioned loan amount limit check ([#26108](https://github.com/frappe/erpnext/pull/26108))
+- Purchase receipt gl entries with same item code ([#26202](https://github.com/frappe/erpnext/pull/26202))
+- Taxable value for invoices with additional discount ([#25906](https://github.com/frappe/erpnext/pull/25906))
+- Correct South Africa VAT Rate (Updated) ([#25894](https://github.com/frappe/erpnext/pull/25894))
+- Remove response_by and resolution_by if sla is removed ([#25997](https://github.com/frappe/erpnext/pull/25997))
+- POS loyalty card alignment ([#26051](https://github.com/frappe/erpnext/pull/26051))
+- Flaky test for Report Subcontracted Raw materials to be transferred ([#26043](https://github.com/frappe/erpnext/pull/26043))
+- Export invoices not visible in GSTR-1 report ([#26143](https://github.com/frappe/erpnext/pull/26143))
+- Account filter not working with accounting dimension filter ([#26211](https://github.com/frappe/erpnext/pull/26211))
+- Allow to select group warehouse while downloading materials from production plan ([#26126](https://github.com/frappe/erpnext/pull/26126))
+- Added freeze when trying to stop work order ([#26192](https://github.com/frappe/erpnext/pull/26192))
+- Time out while submit / cancel the stock transactions with more than 50 Items ([#26081](https://github.com/frappe/erpnext/pull/26081))
+- Address Card issues in e-commerce ([#26187](https://github.com/frappe/erpnext/pull/26187))
+- Error while booking deferred revenue ([#26195](https://github.com/frappe/erpnext/pull/26195))
+- Eliminate repeat creation of HSN codes ([#25947](https://github.com/frappe/erpnext/pull/25947))
+- Opening invoices can alter profit and loss of a closed year ([#25951](https://github.com/frappe/erpnext/pull/25951))
+- Payroll entry employee detail issue ([#25968](https://github.com/frappe/erpnext/pull/25968))
+- Auto tax calculations in Payment Entry ([#26037](https://github.com/frappe/erpnext/pull/26037))
+- Use pos invoice item name as unique identifier ([#26198](https://github.com/frappe/erpnext/pull/26198))
+- Billing address not fetched in Purchase Invoice ([#26100](https://github.com/frappe/erpnext/pull/26100))
+- Timeout while cancelling stock reconciliation ([#26098](https://github.com/frappe/erpnext/pull/26098))
+- Status indicator for delivery notes ([#26062](https://github.com/frappe/erpnext/pull/26062))
+- Unable to enter score in Assessment Result details grid ([#26031](https://github.com/frappe/erpnext/pull/26031))
+- Too many writes while renaming company abbreviation ([#26203](https://github.com/frappe/erpnext/pull/26203))
+- Chart not visible for First Response Time reports ([#26185](https://github.com/frappe/erpnext/pull/26185))
+- Job applicant link issue ([#25934](https://github.com/frappe/erpnext/pull/25934))
+- Fetch preferred shipping address (bp #26132) ([#26201](https://github.com/frappe/erpnext/pull/26201))
diff --git a/erpnext/controllers/stock_controller.py b/erpnext/controllers/stock_controller.py
index 35097b97b9..2526e6df0e 100644
--- a/erpnext/controllers/stock_controller.py
+++ b/erpnext/controllers/stock_controller.py
@@ -11,7 +11,7 @@ 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.utils import check_if_stock_and_account_balance_synced, get_fiscal_year
+from erpnext.accounts.utils import get_fiscal_year
from erpnext.controllers.accounts_controller import AccountsController
from erpnext.stock import get_warehouse_account_map
from erpnext.stock.stock_ledger import get_valuation_rate
@@ -356,42 +356,68 @@ class StockController(AccountsController):
}, update_modified)
def validate_inspection(self):
- '''Checks if quality inspection is set for Items that require inspection.
- On submit, throw an exception'''
- inspection_required_fieldname = None
- if self.doctype in ["Purchase Receipt", "Purchase Invoice"]:
- inspection_required_fieldname = "inspection_required_before_purchase"
- elif self.doctype in ["Delivery Note", "Sales Invoice"]:
- inspection_required_fieldname = "inspection_required_before_delivery"
+ """Checks if quality inspection is set/ is valid for Items that require inspection."""
+ inspection_fieldname_map = {
+ "Purchase Receipt": "inspection_required_before_purchase",
+ "Purchase Invoice": "inspection_required_before_purchase",
+ "Sales Invoice": "inspection_required_before_delivery",
+ "Delivery Note": "inspection_required_before_delivery"
+ }
+ inspection_required_fieldname = inspection_fieldname_map.get(self.doctype)
+ # return if inspection is not required on document level
if ((not inspection_required_fieldname and self.doctype != "Stock Entry") or
(self.doctype == "Stock Entry" and not self.inspection_required) or
(self.doctype in ["Sales Invoice", "Purchase Invoice"] and not self.update_stock)):
return
- for d in self.get('items'):
- qa_required = False
- if (inspection_required_fieldname and not d.quality_inspection and
- frappe.db.get_value("Item", d.item_code, inspection_required_fieldname)):
- qa_required = True
- elif self.doctype == "Stock Entry" and not d.quality_inspection and d.t_warehouse:
- qa_required = True
- if self.docstatus == 1 and d.quality_inspection:
- qa_doc = frappe.get_doc("Quality Inspection", d.quality_inspection)
- if qa_doc.docstatus == 0:
- link = frappe.utils.get_link_to_form('Quality Inspection', d.quality_inspection)
- frappe.throw(_("Quality Inspection: {0} is not submitted for the item: {1} in row {2}").format(link, d.item_code, d.idx), QualityInspectionNotSubmittedError)
+ for row in self.get('items'):
+ qi_required = False
+ if (inspection_required_fieldname and frappe.db.get_value("Item", row.item_code, inspection_required_fieldname)):
+ qi_required = True
+ elif self.doctype == "Stock Entry" and row.t_warehouse:
+ qi_required = True # inward stock needs inspection
- if qa_doc.status != 'Accepted':
- frappe.throw(_("Row {0}: Quality Inspection rejected for item {1}")
- .format(d.idx, d.item_code), QualityInspectionRejectedError)
- elif qa_required :
- action = frappe.get_doc('Stock Settings').action_if_quality_inspection_is_not_submitted
- if self.docstatus==1 and action == 'Stop':
- frappe.throw(_("Quality Inspection required for Item {0} to submit").format(frappe.bold(d.item_code)),
- exc=QualityInspectionRequiredError)
- else:
- frappe.msgprint(_("Create Quality Inspection for Item {0}").format(frappe.bold(d.item_code)))
+ if qi_required: # validate row only if inspection is required on item level
+ self.validate_qi_presence(row)
+ if self.docstatus == 1:
+ self.validate_qi_submission(row)
+ self.validate_qi_rejection(row)
+
+ def validate_qi_presence(self, row):
+ """Check if QI is present on row level. Warn on save and stop on submit if missing."""
+ if not row.quality_inspection:
+ msg = f"Row #{row.idx}: Quality Inspection is required for Item {frappe.bold(row.item_code)}"
+ if self.docstatus == 1:
+ frappe.throw(_(msg), title=_("Inspection Required"), exc=QualityInspectionRequiredError)
+ else:
+ frappe.msgprint(_(msg), title=_("Inspection Required"), indicator="blue")
+
+ def validate_qi_submission(self, row):
+ """Check if QI is submitted on row level, during submission"""
+ action = frappe.db.get_single_value("Stock Settings", "action_if_quality_inspection_is_not_submitted")
+ qa_docstatus = frappe.db.get_value("Quality Inspection", row.quality_inspection, "docstatus")
+
+ if not qa_docstatus == 1:
+ link = frappe.utils.get_link_to_form('Quality Inspection', row.quality_inspection)
+ msg = f"Row #{row.idx}: Quality Inspection {link} is not submitted for the item: {row.item_code}"
+ if action == "Stop":
+ frappe.throw(_(msg), title=_("Inspection Submission"), exc=QualityInspectionNotSubmittedError)
+ else:
+ frappe.msgprint(_(msg), alert=True, indicator="orange")
+
+ def validate_qi_rejection(self, row):
+ """Check if QI is rejected on row level, during submission"""
+ action = frappe.db.get_single_value("Stock Settings", "action_if_quality_inspection_is_rejected")
+ qa_status = frappe.db.get_value("Quality Inspection", row.quality_inspection, "status")
+
+ if qa_status == "Rejected":
+ link = frappe.utils.get_link_to_form('Quality Inspection', row.quality_inspection)
+ msg = f"Row #{row.idx}: Quality Inspection {link} was rejected for item {row.item_code}"
+ if action == "Stop":
+ frappe.throw(_(msg), title=_("Inspection Rejected"), exc=QualityInspectionRejectedError)
+ else:
+ frappe.msgprint(_(msg), alert=True, indicator="orange")
def update_blanket_order(self):
blanket_orders = list(set([d.blanket_order for d in self.items if d.blanket_order]))
@@ -497,9 +523,6 @@ class StockController(AccountsController):
})
if future_sle_exists(args):
create_repost_item_valuation_entry(args)
- elif not is_reposting_pending():
- check_if_stock_and_account_balance_synced(self.posting_date,
- self.company, self.doctype, self.name)
@frappe.whitelist()
def make_quality_inspections(doctype, docname, items):
diff --git a/erpnext/crm/doctype/appointment/appointment.json b/erpnext/crm/doctype/appointment/appointment.json
index 8517ddec32..016b8ec3e4 100644
--- a/erpnext/crm/doctype/appointment/appointment.json
+++ b/erpnext/crm/doctype/appointment/appointment.json
@@ -102,7 +102,7 @@
}
],
"links": [],
- "modified": "2020-01-28 16:16:45.447213",
+ "modified": "2021-06-30 12:09:14.228756",
"modified_by": "Administrator",
"module": "CRM",
"name": "Appointment",
@@ -153,6 +153,18 @@
"role": "Sales User",
"share": 1,
"write": 1
+ },
+ {
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "Employee",
+ "share": 1,
+ "write": 1
}
],
"quick_entry": 1,
diff --git a/erpnext/crm/doctype/lead/lead.py b/erpnext/crm/doctype/lead/lead.py
index d1d096843b..ce3de40fc3 100644
--- a/erpnext/crm/doctype/lead/lead.py
+++ b/erpnext/crm/doctype/lead/lead.py
@@ -168,12 +168,13 @@ class Lead(SellingController):
if self.phone:
contact.append("phone_nos", {
"phone": self.phone,
- "is_primary": 1
+ "is_primary_phone": 1
})
if self.mobile_no:
contact.append("phone_nos", {
- "phone": self.mobile_no
+ "phone": self.mobile_no,
+ "is_primary_mobile_no":1
})
contact.insert(ignore_permissions=True)
diff --git a/erpnext/education/utils.py b/erpnext/education/utils.py
index 9db8a4a90d..3070e6a3e8 100644
--- a/erpnext/education/utils.py
+++ b/erpnext/education/utils.py
@@ -355,11 +355,11 @@ def get_or_create_course_enrollment(course, program):
student = get_current_student()
course_enrollment = get_enrollment("course", course, student.name)
if not course_enrollment:
- program_enrollment = get_enrollment('program', program, student.name)
+ program_enrollment = get_enrollment('program', program.name, student.name)
if not program_enrollment:
frappe.throw(_("You are not enrolled in program {0}").format(program))
return
- return student.enroll_in_course(course_name=course, program_enrollment=get_enrollment('program', program, student.name))
+ return student.enroll_in_course(course_name=course, program_enrollment=get_enrollment('program', program.name, student.name))
else:
return frappe.get_doc('Course Enrollment', course_enrollment)
diff --git a/erpnext/healthcare/doctype/patient_history_settings/test_patient_history_settings.py b/erpnext/healthcare/doctype/patient_history_settings/test_patient_history_settings.py
index c93b788aed..33119d8185 100644
--- a/erpnext/healthcare/doctype/patient_history_settings/test_patient_history_settings.py
+++ b/erpnext/healthcare/doctype/patient_history_settings/test_patient_history_settings.py
@@ -6,7 +6,7 @@ from __future__ import unicode_literals
import frappe
import unittest
import json
-from frappe.utils import getdate
+from frappe.utils import getdate, strip_html
from erpnext.healthcare.doctype.patient_appointment.test_patient_appointment import create_patient
class TestPatientHistorySettings(unittest.TestCase):
@@ -44,9 +44,9 @@ class TestPatientHistorySettings(unittest.TestCase):
self.assertTrue(medical_rec)
medical_rec = frappe.get_doc("Patient Medical Record", medical_rec)
- expected_subject = "Date: {0}
Rating: 3
Feedback: Test Patient History Settings
".format(
+ expected_subject = "Date: {0}Rating: 3Feedback: Test Patient History Settings".format(
frappe.utils.format_date(getdate()))
- self.assertEqual(medical_rec.subject, expected_subject)
+ self.assertEqual(strip_html(medical_rec.subject), expected_subject)
self.assertEqual(medical_rec.patient, patient)
self.assertEqual(medical_rec.communication_date, getdate())
@@ -101,4 +101,4 @@ def create_doc(patient):
}).insert()
doc.submit()
- return doc
\ No newline at end of file
+ return doc
diff --git a/erpnext/hooks.py b/erpnext/hooks.py
index 3da606b68b..ba10b58f85 100644
--- a/erpnext/hooks.py
+++ b/erpnext/hooks.py
@@ -158,6 +158,7 @@ website_route_rules = [
"parents": [{"label": _("Material Request"), "route": "material-requests"}]
}
},
+ {"from_route": "/project", "to_route": "Project"}
]
standard_portal_menu_items = [
diff --git a/erpnext/hr/doctype/leave_allocation/leave_allocation.json b/erpnext/hr/doctype/leave_allocation/leave_allocation.json
index ae009ba3e1..3a6539ece9 100644
--- a/erpnext/hr/doctype/leave_allocation/leave_allocation.json
+++ b/erpnext/hr/doctype/leave_allocation/leave_allocation.json
@@ -110,7 +110,7 @@
"label": "Allocation"
},
{
- "allow_on_submit": 1,
+ "allow_on_submit": 1,
"bold": 1,
"fieldname": "new_leaves_allocated",
"fieldtype": "Float",
diff --git a/erpnext/hr/doctype/leave_allocation/test_leave_allocation.py b/erpnext/hr/doctype/leave_allocation/test_leave_allocation.py
index 49dd701a1e..fdcd533660 100644
--- a/erpnext/hr/doctype/leave_allocation/test_leave_allocation.py
+++ b/erpnext/hr/doctype/leave_allocation/test_leave_allocation.py
@@ -164,7 +164,6 @@ class TestLeaveAllocation(unittest.TestCase):
leave_allocation.cancel()
self.assertFalse(frappe.db.exists("Leave Ledger Entry", {'transaction_name':leave_allocation.name}))
-
def test_leave_addition_after_submit(self):
frappe.db.sql("delete from `tabLeave Allocation`")
frappe.db.sql("delete from `tabLeave Ledger Entry`")
@@ -179,7 +178,6 @@ class TestLeaveAllocation(unittest.TestCase):
def test_leave_subtraction_after_submit(self):
frappe.db.sql("delete from `tabLeave Allocation`")
frappe.db.sql("delete from `tabLeave Ledger Entry`")
-
leave_allocation = create_leave_allocation()
leave_allocation.submit()
self.assertTrue(leave_allocation.total_leaves_allocated, 15)
@@ -199,8 +197,8 @@ class TestLeaveAllocation(unittest.TestCase):
"doctype": 'Leave Application',
"employee": employee.name,
"leave_type": "_Test Leave Type",
- "from_date": nowdate(),
- "to_date": add_days(nowdate(), 10),
+ "from_date": add_months(nowdate(), 2),
+ "to_date": add_months(add_days(nowdate(), 10), 2),
"company": erpnext.get_default_company() or "_Test Company",
"docstatus": 1,
"status": "Approved",
diff --git a/erpnext/hr/doctype/training_event/training_event.js b/erpnext/hr/doctype/training_event/training_event.js
index b7d34b178a..d5f6e5f573 100644
--- a/erpnext/hr/doctype/training_event/training_event.js
+++ b/erpnext/hr/doctype/training_event/training_event.js
@@ -20,11 +20,10 @@ frappe.ui.form.on('Training Event', {
frappe.set_route("List", "Training Feedback");
});
}
- }
-});
+ frm.events.set_employee_query(frm);
+ },
-frappe.ui.form.on("Training Event Employee", {
- employee: function (frm) {
+ set_employee_query: function(frm) {
let emp = [];
for (let d in frm.doc.employees) {
if (frm.doc.employees[d].employee) {
@@ -34,9 +33,17 @@ frappe.ui.form.on("Training Event Employee", {
frm.set_query("employee", "employees", function () {
return {
filters: {
- name: ["NOT IN", emp]
+ name: ["NOT IN", emp],
+ status: "Active"
}
};
});
}
});
+
+frappe.ui.form.on("Training Event Employee", {
+ employee: function(frm) {
+ frm.events.set_employee_query(frm);
+ }
+});
+
diff --git a/erpnext/hr/doctype/training_event_employee/training_event_employee.json b/erpnext/hr/doctype/training_event_employee/training_event_employee.json
index 2d313e9fac..bcb7d5e5bc 100644
--- a/erpnext/hr/doctype/training_event_employee/training_event_employee.json
+++ b/erpnext/hr/doctype/training_event_employee/training_event_employee.json
@@ -19,6 +19,7 @@
"fieldtype": "Link",
"in_list_view": 1,
"label": "Employee",
+ "no_copy": 1,
"options": "Employee"
},
{
@@ -68,7 +69,7 @@
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
- "modified": "2021-05-21 12:41:59.336237",
+ "modified": "2021-07-02 17:20:27.630176",
"modified_by": "Administrator",
"module": "HR",
"name": "Training Event Employee",
diff --git a/erpnext/manufacturing/doctype/bom/bom.js b/erpnext/manufacturing/doctype/bom/bom.js
index 44f841f13b..c56668840e 100644
--- a/erpnext/manufacturing/doctype/bom/bom.js
+++ b/erpnext/manufacturing/doctype/bom/bom.js
@@ -71,7 +71,6 @@ frappe.ui.form.on("BOM", {
refresh: function(frm) {
frm.toggle_enable("item", frm.doc.__islocal);
- toggle_operations(frm);
frm.set_indicator_formatter('item_code',
function(doc) {
@@ -326,8 +325,7 @@ frappe.ui.form.on("BOM", {
freeze: true,
args: {
update_parent: true,
- from_child_bom:false,
- save: frm.doc.docstatus === 1 ? true : false
+ from_child_bom:false
},
callback: function(r) {
refresh_field("items");
@@ -651,15 +649,8 @@ frappe.ui.form.on("BOM Item", "items_remove", function(frm) {
erpnext.bom.calculate_total(frm.doc);
});
-var toggle_operations = function(frm) {
- frm.toggle_display("operations_section", cint(frm.doc.with_operations) == 1);
- frm.toggle_display("transfer_material_against", cint(frm.doc.with_operations) == 1);
- frm.toggle_reqd("transfer_material_against", cint(frm.doc.with_operations) == 1);
-};
-
frappe.ui.form.on("BOM", "with_operations", function(frm) {
if(!cint(frm.doc.with_operations)) {
frm.set_value("operations", []);
}
- toggle_operations(frm);
});
diff --git a/erpnext/manufacturing/doctype/bom/bom.json b/erpnext/manufacturing/doctype/bom/bom.json
index f551b91597..f38d1b9892 100644
--- a/erpnext/manufacturing/doctype/bom/bom.json
+++ b/erpnext/manufacturing/doctype/bom/bom.json
@@ -193,6 +193,7 @@
},
{
"default": "Work Order",
+ "depends_on": "with_operations",
"fieldname": "transfer_material_against",
"fieldtype": "Select",
"label": "Transfer Material Against",
@@ -235,6 +236,7 @@
{
"fieldname": "operations_section",
"fieldtype": "Section Break",
+ "hide_border": 1,
"oldfieldtype": "Section Break"
},
{
@@ -245,6 +247,7 @@
"options": "Routing"
},
{
+ "depends_on": "with_operations",
"fieldname": "operations",
"fieldtype": "Table",
"label": "Operations",
@@ -517,7 +520,7 @@
"image_field": "image",
"is_submittable": 1,
"links": [],
- "modified": "2020-05-21 12:29:32.634952",
+ "modified": "2021-03-16 12:25:09.081968",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "BOM",
diff --git a/erpnext/manufacturing/doctype/bom/bom.py b/erpnext/manufacturing/doctype/bom/bom.py
index 9bb4028374..3bd1fe6c7f 100644
--- a/erpnext/manufacturing/doctype/bom/bom.py
+++ b/erpnext/manufacturing/doctype/bom/bom.py
@@ -662,7 +662,7 @@ class BOM(WebsiteGenerator):
self.get_routing()
def validate_operations(self):
- if self.with_operations and not self.get('operations'):
+ if self.with_operations and not self.get('operations') and self.docstatus == 1:
frappe.throw(_("Operations cannot be left blank"))
if self.with_operations:
@@ -1100,6 +1100,8 @@ def make_variant_bom(source_name, bom_no, item, variant_items, target_doc=None):
},
'BOM Item': {
'doctype': 'BOM Item',
+ # stop get_mapped_doc copying parent bom_no to children
+ 'field_no_map': ['bom_no'],
'condition': lambda doc: doc.has_variants == 0
},
}, target_doc, postprocess)
diff --git a/erpnext/manufacturing/doctype/bom/test_bom.py b/erpnext/manufacturing/doctype/bom/test_bom.py
index 57a5458726..c89f7d66fd 100644
--- a/erpnext/manufacturing/doctype/bom/test_bom.py
+++ b/erpnext/manufacturing/doctype/bom/test_bom.py
@@ -8,6 +8,7 @@ import frappe
from frappe.utils import cstr, flt
from frappe.test_runner import make_test_records
from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation import create_stock_reconciliation
+from erpnext.manufacturing.doctype.bom.bom import make_variant_bom
from erpnext.manufacturing.doctype.bom_update_tool.bom_update_tool import update_cost
from erpnext.stock.doctype.item.test_item import make_item
from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order
@@ -248,6 +249,37 @@ class TestBOM(unittest.TestCase):
for reqd_item, created_item in zip(reqd_order, created_order):
self.assertEqual(reqd_item, created_item.item_code)
+ def test_generated_variant_bom(self):
+ from erpnext.controllers.item_variant import create_variant
+
+ template_item = make_item(
+ "_TestTemplateItem", {"has_variants": 1, "attributes": [{"attribute": "Test Size"},]}
+ )
+ variant = create_variant(template_item.item_code, {"Test Size": "Large"})
+ variant.insert(ignore_if_duplicate=True)
+
+ bom_tree = {
+ template_item.item_code: {
+ "SubAssembly1": {"ChildPart1": {}, "ChildPart2": {},},
+ "ChildPart5": {},
+ }
+ }
+ template_bom = create_nested_bom(bom_tree, prefix="")
+ variant_bom = make_variant_bom(
+ template_bom.name, template_bom.name, variant.item_code, variant_items=[]
+ )
+ variant_bom.save()
+
+ reqd_order = template_bom.get_tree_representation().level_order_traversal()
+ created_order = variant_bom.get_tree_representation().level_order_traversal()
+
+ self.assertEqual(len(reqd_order), len(created_order))
+
+ for reqd_item, created_item in zip(reqd_order, created_order):
+ self.assertEqual(reqd_item.item_code, created_item.item_code)
+ self.assertEqual(reqd_item.qty, created_item.qty)
+ self.assertEqual(reqd_item.exploded_qty, created_item.exploded_qty)
+
def get_default_bom(item_code="_Test FG Item 2"):
return frappe.db.get_value("BOM", {"item": item_code, "is_active": 1, "is_default": 1})
diff --git a/erpnext/manufacturing/doctype/bom_operation/bom_operation.json b/erpnext/manufacturing/doctype/bom_operation/bom_operation.json
index 07464e3e76..4458e6db23 100644
--- a/erpnext/manufacturing/doctype/bom_operation/bom_operation.json
+++ b/erpnext/manufacturing/doctype/bom_operation/bom_operation.json
@@ -13,10 +13,10 @@
"col_break1",
"hour_rate",
"time_in_mins",
- "batch_size",
"operating_cost",
"base_hour_rate",
"base_operating_cost",
+ "batch_size",
"image"
],
"fields": [
@@ -61,6 +61,8 @@
},
{
"description": "In minutes",
+ "fetch_from": "operation.total_operation_time",
+ "fetch_if_empty": 1,
"fieldname": "time_in_mins",
"fieldtype": "Float",
"in_list_view": 1,
@@ -104,7 +106,8 @@
"label": "Image"
},
{
- "default": "1",
+ "fetch_from": "operation.batch_size",
+ "fetch_if_empty": 1,
"fieldname": "batch_size",
"fieldtype": "Int",
"label": "Batch Size"
@@ -120,7 +123,7 @@
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
- "modified": "2020-10-13 18:14:10.018774",
+ "modified": "2021-01-12 14:48:09.596843",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "BOM Operation",
diff --git a/erpnext/manufacturing/doctype/job_card/job_card.js b/erpnext/manufacturing/doctype/job_card/job_card.js
index 4e8dd41022..81860c9fbc 100644
--- a/erpnext/manufacturing/doctype/job_card/job_card.js
+++ b/erpnext/manufacturing/doctype/job_card/job_card.js
@@ -11,6 +11,16 @@ frappe.ui.form.on('Job Card', {
}
};
});
+
+ frm.set_indicator_formatter('sub_operation',
+ function(doc) {
+ if (doc.status == "Pending") {
+ return "red";
+ } else {
+ return doc.status === "Complete" ? "green" : "orange";
+ }
+ }
+ );
},
refresh: function(frm) {
@@ -31,6 +41,10 @@ frappe.ui.form.on('Job Card', {
}
}
+ if (frm.doc.docstatus == 1 && !frm.doc.is_corrective_job_card) {
+ frm.trigger('setup_corrective_job_card');
+ }
+
frm.set_query("quality_inspection", function() {
return {
query: "erpnext.stock.doctype.quality_inspection.quality_inspection.quality_inspection_query",
@@ -43,12 +57,62 @@ frappe.ui.form.on('Job Card', {
frm.trigger("toggle_operation_number");
- if (frm.doc.docstatus == 0 && (frm.doc.for_quantity > frm.doc.total_completed_qty || !frm.doc.for_quantity)
+ if (frm.doc.docstatus == 0 && !frm.is_new() &&
+ (frm.doc.for_quantity > frm.doc.total_completed_qty || !frm.doc.for_quantity)
&& (frm.doc.items || !frm.doc.items.length || frm.doc.for_quantity == frm.doc.transferred_qty)) {
frm.trigger("prepare_timer_buttons");
}
},
+ setup_corrective_job_card: function(frm) {
+ frm.add_custom_button(__('Corrective Job Card'), () => {
+ let operations = frm.doc.sub_operations.map(d => d.sub_operation).concat(frm.doc.operation);
+
+ let fields = [
+ {
+ fieldtype: 'Link', label: __('Corrective Operation'), options: 'Operation',
+ fieldname: 'operation', get_query() {
+ return {
+ filters: {
+ "is_corrective_operation": 1
+ }
+ };
+ }
+ }, {
+ fieldtype: 'Link', label: __('For Operation'), options: 'Operation',
+ fieldname: 'for_operation', get_query() {
+ return {
+ filters: {
+ "name": ["in", operations]
+ }
+ };
+ }
+ }
+ ];
+
+ frappe.prompt(fields, d => {
+ frm.events.make_corrective_job_card(frm, d.operation, d.for_operation);
+ }, __("Select Corrective Operation"));
+ }, __('Make'));
+ },
+
+ make_corrective_job_card: function(frm, operation, for_operation) {
+ frappe.call({
+ method: 'erpnext.manufacturing.doctype.job_card.job_card.make_corrective_job_card',
+ args: {
+ source_name: frm.doc.name,
+ operation: operation,
+ for_operation: for_operation
+ },
+ callback: function(r) {
+ if (r.message) {
+ frappe.model.sync(r.message);
+ frappe.set_route("Form", r.message.doctype, r.message.name);
+ }
+ }
+ });
+ },
+
operation: function(frm) {
frm.trigger("toggle_operation_number");
@@ -97,101 +161,105 @@ frappe.ui.form.on('Job Card', {
prepare_timer_buttons: function(frm) {
frm.trigger("make_dashboard");
- if (!frm.doc.job_started) {
- frm.add_custom_button(__("Start"), () => {
- if (!frm.doc.employee) {
- frappe.prompt({fieldtype: 'Link', label: __('Employee'), options: "Employee",
- fieldname: 'employee'}, d => {
- if (d.employee) {
- frm.set_value("employee", d.employee);
- } else {
- frm.events.start_job(frm);
- }
- }, __("Enter Value"), __("Start"));
+
+ if (!frm.doc.started_time && !frm.doc.current_time) {
+ frm.add_custom_button(__("Start Job"), () => {
+ if ((frm.doc.employee && !frm.doc.employee.length) || !frm.doc.employee) {
+ frappe.prompt({fieldtype: 'Table MultiSelect', label: __('Select Employees'),
+ options: "Job Card Time Log", fieldname: 'employees'}, d => {
+ frm.events.start_job(frm, "Work In Progress", d.employees);
+ }, __("Assign Job to Employee"));
} else {
- frm.events.start_job(frm);
+ frm.events.start_job(frm, "Work In Progress", frm.doc.employee);
}
}).addClass("btn-primary");
} else if (frm.doc.status == "On Hold") {
- frm.add_custom_button(__("Resume"), () => {
- frappe.flags.resume_job = 1;
- frm.events.start_job(frm);
+ frm.add_custom_button(__("Resume Job"), () => {
+ frm.events.start_job(frm, "Resume Job", frm.doc.employee);
}).addClass("btn-primary");
} else {
- frm.add_custom_button(__("Pause"), () => {
- frappe.flags.pause_job = 1;
- frm.set_value("status", "On Hold");
- frm.events.complete_job(frm);
+ frm.add_custom_button(__("Pause Job"), () => {
+ frm.events.complete_job(frm, "On Hold");
});
- frm.add_custom_button(__("Complete"), () => {
- let completed_time = frappe.datetime.now_datetime();
- frm.trigger("hide_timer");
+ frm.add_custom_button(__("Complete Job"), () => {
+ var sub_operations = frm.doc.sub_operations;
- if (frm.doc.for_quantity) {
+ let set_qty = true;
+ if (sub_operations && sub_operations.length > 1) {
+ set_qty = false;
+ let last_op_row = sub_operations[sub_operations.length - 2];
+
+ if (last_op_row.status == 'Complete') {
+ set_qty = true;
+ }
+ }
+
+ if (set_qty) {
frappe.prompt({fieldtype: 'Float', label: __('Completed Quantity'),
- fieldname: 'qty', reqd: 1, default: frm.doc.for_quantity}, data => {
- frm.events.complete_job(frm, completed_time, data.qty);
- }, __("Enter Value"), __("Complete"));
+ fieldname: 'qty', default: frm.doc.for_quantity}, data => {
+ frm.events.complete_job(frm, "Complete", data.qty);
+ }, __("Enter Value"));
} else {
- frm.events.complete_job(frm, completed_time, 0);
+ frm.events.complete_job(frm, "Complete", 0.0);
}
}).addClass("btn-primary");
}
},
- start_job: function(frm) {
- let row = frappe.model.add_child(frm.doc, 'Job Card Time Log', 'time_logs');
- row.from_time = frappe.datetime.now_datetime();
- frm.set_value('job_started', 1);
- frm.set_value('started_time' , row.from_time);
- frm.set_value("status", "Work In Progress");
-
- if (!frappe.flags.resume_job) {
- frm.set_value('current_time' , 0);
- }
-
- frm.save();
+ start_job: function(frm, status, employee) {
+ const args = {
+ job_card_id: frm.doc.name,
+ start_time: frappe.datetime.now_datetime(),
+ employees: employee,
+ status: status
+ };
+ frm.events.make_time_log(frm, args);
},
- complete_job: function(frm, completed_time, completed_qty) {
- frm.doc.time_logs.forEach(d => {
- if (d.from_time && !d.to_time) {
- d.to_time = completed_time || frappe.datetime.now_datetime();
- d.completed_qty = completed_qty || 0;
+ complete_job: function(frm, status, completed_qty) {
+ const args = {
+ job_card_id: frm.doc.name,
+ complete_time: frappe.datetime.now_datetime(),
+ status: status,
+ completed_qty: completed_qty
+ };
+ frm.events.make_time_log(frm, args);
+ },
- if(frappe.flags.pause_job) {
- let currentIncrement = moment(d.to_time).diff(moment(d.from_time),"seconds") || 0;
- frm.set_value('current_time' , currentIncrement + (frm.doc.current_time || 0));
- } else {
- frm.set_value('started_time' , '');
- frm.set_value('job_started', 0);
- frm.set_value('current_time' , 0);
- }
+ make_time_log: function(frm, args) {
+ frm.events.update_sub_operation(frm, args);
- frm.save();
+ frappe.call({
+ method: "erpnext.manufacturing.doctype.job_card.job_card.make_time_log",
+ args: {
+ args: args
+ },
+ freeze: true,
+ callback: function () {
+ frm.reload_doc();
+ frm.trigger("make_dashboard");
}
});
},
+ update_sub_operation: function(frm, args) {
+ if (frm.doc.sub_operations && frm.doc.sub_operations.length) {
+ let sub_operations = frm.doc.sub_operations.filter(d => d.status != 'Complete');
+ if (sub_operations && sub_operations.length) {
+ args["sub_operation"] = sub_operations[0].sub_operation;
+ }
+ }
+ },
+
validate: function(frm) {
if ((!frm.doc.time_logs || !frm.doc.time_logs.length) && frm.doc.started_time) {
frm.trigger("reset_timer");
}
},
- employee: function(frm) {
- if (frm.doc.job_started && !frm.doc.current_time) {
- frm.trigger("reset_timer");
- } else {
- frm.events.start_job(frm);
- }
- },
-
reset_timer: function(frm) {
frm.set_value('started_time' , '');
- frm.set_value('job_started', 0);
- frm.set_value('current_time' , 0);
},
make_dashboard: function(frm) {
@@ -297,7 +365,6 @@ frappe.ui.form.on('Job Card Time Log', {
},
to_time: function(frm) {
- frm.set_value('job_started', 0);
frm.set_value('started_time', '');
}
})
\ No newline at end of file
diff --git a/erpnext/manufacturing/doctype/job_card/job_card.json b/erpnext/manufacturing/doctype/job_card/job_card.json
index 5713f697e9..046e2fd182 100644
--- a/erpnext/manufacturing/doctype/job_card/job_card.json
+++ b/erpnext/manufacturing/doctype/job_card/job_card.json
@@ -9,38 +9,49 @@
"naming_series",
"work_order",
"bom_no",
- "workstation",
- "operation",
- "operation_row_number",
"column_break_4",
"posting_date",
"company",
- "remarks",
"production_section",
"production_item",
"item_name",
"for_quantity",
- "quality_inspection",
- "wip_warehouse",
+ "serial_no",
"column_break_12",
- "employee",
- "employee_name",
- "status",
+ "wip_warehouse",
+ "quality_inspection",
"project",
+ "batch_no",
+ "operation_section_section",
+ "operation",
+ "operation_row_number",
+ "column_break_18",
+ "workstation",
+ "employee",
+ "section_break_21",
+ "sub_operations",
"timing_detail",
"time_logs",
"section_break_13",
"total_completed_qty",
- "total_time_in_mins",
"column_break_15",
+ "total_time_in_mins",
"section_break_8",
"items",
+ "corrective_operation_section",
+ "for_job_card",
+ "is_corrective_job_card",
+ "column_break_33",
+ "hour_rate",
+ "for_operation",
"more_information",
"operation_id",
"sequence_id",
"transferred_qty",
"requested_qty",
+ "status",
"column_break_20",
+ "remarks",
"barcode",
"job_started",
"started_time",
@@ -117,13 +128,6 @@
"fieldtype": "Section Break",
"label": "Timing Detail"
},
- {
- "fieldname": "employee",
- "fieldtype": "Link",
- "in_standard_filter": 1,
- "label": "Employee",
- "options": "Employee"
- },
{
"allow_bulk_edit": 1,
"fieldname": "time_logs",
@@ -133,9 +137,11 @@
},
{
"fieldname": "section_break_13",
- "fieldtype": "Section Break"
+ "fieldtype": "Section Break",
+ "hide_border": 1
},
{
+ "default": "0",
"fieldname": "total_completed_qty",
"fieldtype": "Float",
"label": "Total Completed Qty",
@@ -160,8 +166,7 @@
"fieldname": "items",
"fieldtype": "Table",
"label": "Items",
- "options": "Job Card Item",
- "read_only": 1
+ "options": "Job Card Item"
},
{
"collapsible": 1,
@@ -251,12 +256,7 @@
"reqd": 1
},
{
- "fetch_from": "employee.employee_name",
- "fieldname": "employee_name",
- "fieldtype": "Read Only",
- "label": "Employee Name"
- },
- {
+ "collapsible": 1,
"fieldname": "production_section",
"fieldtype": "Section Break",
"label": "Production"
@@ -314,11 +314,89 @@
"label": "Quality Inspection",
"no_copy": 1,
"options": "Quality Inspection"
+ },
+ {
+ "allow_bulk_edit": 1,
+ "fieldname": "sub_operations",
+ "fieldtype": "Table",
+ "label": "Sub Operations",
+ "options": "Job Card Operation",
+ "read_only": 1
+ },
+ {
+ "fieldname": "operation_section_section",
+ "fieldtype": "Section Break",
+ "label": "Operation Section"
+ },
+ {
+ "fieldname": "column_break_18",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "section_break_21",
+ "fieldtype": "Section Break",
+ "hide_border": 1
+ },
+ {
+ "depends_on": "is_corrective_job_card",
+ "fieldname": "hour_rate",
+ "fieldtype": "Currency",
+ "label": "Hour Rate"
+ },
+ {
+ "collapsible": 1,
+ "depends_on": "is_corrective_job_card",
+ "fieldname": "corrective_operation_section",
+ "fieldtype": "Section Break",
+ "label": "Corrective Operation"
+ },
+ {
+ "default": "0",
+ "fieldname": "is_corrective_job_card",
+ "fieldtype": "Check",
+ "label": "Is Corrective Job Card",
+ "read_only": 1
+ },
+ {
+ "fieldname": "column_break_33",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "for_job_card",
+ "fieldtype": "Link",
+ "label": "For Job Card",
+ "options": "Job Card",
+ "read_only": 1
+ },
+ {
+ "fetch_from": "for_job_card.operation",
+ "fetch_if_empty": 1,
+ "fieldname": "for_operation",
+ "fieldtype": "Link",
+ "label": "For Operation",
+ "options": "Operation"
+ },
+ {
+ "fieldname": "employee",
+ "fieldtype": "Table MultiSelect",
+ "label": "Employee",
+ "options": "Job Card Time Log"
+ },
+ {
+ "fieldname": "serial_no",
+ "fieldtype": "Small Text",
+ "label": "Serial No"
+ },
+ {
+ "fieldname": "batch_no",
+ "fieldtype": "Link",
+ "label": "Batch No",
+ "options": "Batch"
}
],
"is_submittable": 1,
"links": [],
- "modified": "2020-11-19 18:26:50.531664",
+ "modified": "2021-03-16 15:59:32.766484",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "Job Card",
diff --git a/erpnext/manufacturing/doctype/job_card/job_card.py b/erpnext/manufacturing/doctype/job_card/job_card.py
index cdc4518894..7f8f2ef68d 100644
--- a/erpnext/manufacturing/doctype/job_card/job_card.py
+++ b/erpnext/manufacturing/doctype/job_card/job_card.py
@@ -5,11 +5,12 @@
from __future__ import unicode_literals
import frappe
import datetime
+import json
from frappe import _, bold
from frappe.model.mapper import get_mapped_doc
from frappe.model.document import Document
from frappe.utils import (flt, cint, time_diff_in_hours, get_datetime, getdate,
- get_time, add_to_date, time_diff, add_days, get_datetime_str, get_link_to_form)
+ get_time, add_to_date, time_diff, add_days, get_datetime_str, get_link_to_form, time_diff_in_seconds)
from erpnext.manufacturing.doctype.manufacturing_settings.manufacturing_settings import get_mins_between_operations
@@ -25,10 +26,21 @@ class JobCard(Document):
self.set_status()
self.validate_operation_id()
self.validate_sequence_id()
+ self.get_sub_operations()
+ self.update_sub_operation_status()
+
+ def get_sub_operations(self):
+ if self.operation:
+ self.sub_operations = []
+ for row in frappe.get_all("Sub Operation",
+ filters = {"parent": self.operation}, fields=["operation", "idx"]):
+ row.status = "Pending"
+ row.sub_operation = row.operation
+ self.append("sub_operations", row)
def validate_time_logs(self):
- self.total_completed_qty = 0.0
self.total_time_in_mins = 0.0
+ self.total_completed_qty = 0.0
if self.get('time_logs'):
for d in self.get('time_logs'):
@@ -44,11 +56,14 @@ class JobCard(Document):
d.time_in_mins = time_diff_in_hours(d.to_time, d.from_time) * 60
self.total_time_in_mins += d.time_in_mins
- if d.completed_qty:
+ if d.completed_qty and not self.sub_operations:
self.total_completed_qty += d.completed_qty
self.total_completed_qty = flt(self.total_completed_qty, self.precision("total_completed_qty"))
+ for row in self.sub_operations:
+ self.total_completed_qty += row.completed_qty
+
def get_overlap_for(self, args, check_next_available_slot=False):
production_capacity = 1
@@ -57,7 +72,7 @@ class JobCard(Document):
self.workstation, 'production_capacity') or 1
validate_overlap_for = " and jc.workstation = %(workstation)s "
- if self.employee:
+ if args.get("employee"):
# override capacity for employee
production_capacity = 1
validate_overlap_for = " and jc.employee = %(employee)s "
@@ -80,7 +95,7 @@ class JobCard(Document):
"to_time": args.to_time,
"name": args.name or "No Name",
"parent": args.parent or "No Name",
- "employee": self.employee,
+ "employee": args.get("employee"),
"workstation": self.workstation
}, as_dict=True)
@@ -158,6 +173,100 @@ class JobCard(Document):
row.planned_start_time = datetime.datetime.combine(start_date,
get_time(workstation_doc.working_hours[0].start_time))
+ def add_time_log(self, args):
+ last_row = []
+ employees = args.employees
+ if isinstance(employees, str):
+ employees = json.loads(employees)
+
+ if self.time_logs and len(self.time_logs) > 0:
+ last_row = self.time_logs[-1]
+
+ self.reset_timer_value(args)
+ if last_row and args.get("complete_time"):
+ for row in self.time_logs:
+ if not row.to_time:
+ row.update({
+ "to_time": get_datetime(args.get("complete_time")),
+ "operation": args.get("sub_operation"),
+ "completed_qty": args.get("completed_qty") or 0.0
+ })
+ elif args.get("start_time"):
+ for name in employees:
+ self.append("time_logs", {
+ "from_time": get_datetime(args.get("start_time")),
+ "employee": name.get('employee'),
+ "operation": args.get("sub_operation"),
+ "completed_qty": 0.0
+ })
+
+ if not self.employee:
+ self.set_employees(employees)
+
+ if self.status == "On Hold":
+ self.current_time = time_diff_in_seconds(last_row.to_time, last_row.from_time)
+
+ self.save()
+
+ def set_employees(self, employees):
+ for name in employees:
+ self.append('employee', {
+ 'employee': name.get('employee'),
+ 'completed_qty': 0.0
+ })
+
+ def reset_timer_value(self, args):
+ self.started_time = None
+
+ if args.get("status") in ["Work In Progress", "Complete"]:
+ self.current_time = 0.0
+
+ if args.get("status") == "Work In Progress":
+ self.started_time = get_datetime(args.get("start_time"))
+
+ if args.get("status") == "Resume Job":
+ args["status"] = "Work In Progress"
+
+ if args.get("status"):
+ self.status = args.get("status")
+
+ def update_sub_operation_status(self):
+ if not (self.sub_operations and self.time_logs):
+ return
+
+ operation_wise_completed_time = {}
+ for time_log in self.time_logs:
+ if time_log.operation not in operation_wise_completed_time:
+ operation_wise_completed_time.setdefault(time_log.operation,
+ frappe._dict({"status": "Pending", "completed_qty":0.0, "completed_time": 0.0, "employee": []}))
+
+ op_row = operation_wise_completed_time[time_log.operation]
+ op_row.status = "Work In Progress" if not time_log.time_in_mins else "Complete"
+ if self.status == 'On Hold':
+ op_row.status = 'Pause'
+
+ op_row.employee.append(time_log.employee)
+ if time_log.time_in_mins:
+ op_row.completed_time += time_log.time_in_mins
+ op_row.completed_qty += time_log.completed_qty
+
+ for row in self.sub_operations:
+ operation_deatils = operation_wise_completed_time.get(row.sub_operation)
+ if operation_deatils:
+ if row.status != 'Complete':
+ row.status = operation_deatils.status
+
+ row.completed_time = operation_deatils.completed_time
+ if operation_deatils.employee:
+ row.completed_time = row.completed_time / len(set(operation_deatils.employee))
+
+ if operation_deatils.completed_qty:
+ row.completed_qty = operation_deatils.completed_qty / len(set(operation_deatils.employee))
+ else:
+ row.status = 'Pending'
+ row.completed_time = 0.0
+ row.completed_qty = 0.0
+
def update_time_logs(self, row):
self.append("time_logs", {
"from_time": row.planned_start_time,
@@ -182,15 +291,18 @@ class JobCard(Document):
if self.get('operation') == d.operation:
self.append('items', {
- 'item_code': d.item_code,
- 'source_warehouse': d.source_warehouse,
- 'uom': frappe.db.get_value("Item", d.item_code, 'stock_uom'),
- 'item_name': d.item_name,
- 'description': d.description,
- 'required_qty': (d.required_qty * flt(self.for_quantity)) / doc.qty
+ "item_code": d.item_code,
+ "source_warehouse": d.source_warehouse,
+ "uom": frappe.db.get_value("Item", d.item_code, 'stock_uom'),
+ "item_name": d.item_name,
+ "description": d.description,
+ "required_qty": (d.required_qty * flt(self.for_quantity)) / doc.qty,
+ "rate": d.rate,
+ "amount": d.amount
})
def on_submit(self):
+ self.validate_transfer_qty()
self.validate_job_card()
self.update_work_order()
self.set_transferred_qty()
@@ -199,7 +311,16 @@ class JobCard(Document):
self.update_work_order()
self.set_transferred_qty()
+ def validate_transfer_qty(self):
+ if self.items and self.transferred_qty < self.for_quantity:
+ frappe.throw(_('Materials needs to be transferred to the work in progress warehouse for the job card {0}')
+ .format(self.name))
+
def validate_job_card(self):
+ if self.work_order and frappe.get_cached_value('Work Order', self.work_order, 'status') == 'Stopped':
+ frappe.throw(_("Transaction not allowed against stopped Work Order {0}")
+ .format(get_link_to_form('Work Order', self.work_order)))
+
if not self.time_logs:
frappe.throw(_("Time logs are required for {0} {1}")
.format(bold("Job Card"), get_link_to_form("Job Card", self.name)))
@@ -215,6 +336,10 @@ class JobCard(Document):
if not self.work_order:
return
+ if self.is_corrective_job_card and not cint(frappe.db.get_single_value('Manufacturing Settings',
+ 'add_corrective_operation_cost_in_finished_good_valuation')):
+ return
+
for_quantity, time_in_mins = 0, 0
from_time_list, to_time_list = [], []
@@ -225,10 +350,24 @@ class JobCard(Document):
time_in_mins = flt(data[0].time_in_mins)
wo = frappe.get_doc('Work Order', self.work_order)
- if self.operation_id:
+
+ if self.is_corrective_job_card:
+ self.update_corrective_in_work_order(wo)
+
+ elif self.operation_id:
self.validate_produced_quantity(for_quantity, wo)
self.update_work_order_data(for_quantity, time_in_mins, wo)
+ def update_corrective_in_work_order(self, wo):
+ wo.corrective_operation_cost = 0.0
+ for row in frappe.get_all('Job Card', fields = ['total_time_in_mins', 'hour_rate'],
+ filters = {'is_corrective_job_card': 1, 'docstatus': 1, 'work_order': self.work_order}):
+ wo.corrective_operation_cost += flt(row.total_time_in_mins) * flt(row.hour_rate)
+
+ wo.calculate_operating_cost()
+ wo.flags.ignore_validate_update_after_submit = True
+ wo.save()
+
def validate_produced_quantity(self, for_quantity, wo):
if self.docstatus < 2: return
@@ -248,8 +387,8 @@ class JobCard(Document):
min(from_time) as start_time, max(to_time) as end_time
FROM `tabJob Card` jc, `tabJob Card Time Log` jctl
WHERE
- jctl.parent = jc.name and jc.work_order = %s
- and jc.operation_id = %s and jc.docstatus = 1
+ jctl.parent = jc.name and jc.work_order = %s and jc.operation_id = %s
+ and jc.docstatus = 1 and IFNULL(jc.is_corrective_job_card, 0) = 0
""", (self.work_order, self.operation_id), as_dict=1)
for data in wo.operations:
@@ -271,7 +410,8 @@ class JobCard(Document):
def get_current_operation_data(self):
return frappe.get_all('Job Card',
fields = ["sum(total_time_in_mins) as time_in_mins", "sum(total_completed_qty) as completed_qty"],
- filters = {"docstatus": 1, "work_order": self.work_order, "operation_id": self.operation_id})
+ filters = {"docstatus": 1, "work_order": self.work_order, "operation_id": self.operation_id,
+ "is_corrective_job_card": 0})
def set_transferred_qty_in_job_card(self, ste_doc):
for row in ste_doc.items:
@@ -354,7 +494,11 @@ class JobCard(Document):
.format(bold(self.operation), work_order), OperationMismatchError)
def validate_sequence_id(self):
- if not (self.work_order and self.sequence_id): return
+ if self.is_corrective_job_card:
+ return
+
+ if not (self.work_order and self.sequence_id):
+ return
current_operation_qty = 0.0
data = self.get_current_operation_data()
@@ -376,6 +520,17 @@ class JobCard(Document):
frappe.throw(_("{0}, complete the operation {1} before the operation {2}.")
.format(message, bold(row.operation), bold(self.operation)), OperationSequenceError)
+
+@frappe.whitelist()
+def make_time_log(args):
+ if isinstance(args, str):
+ args = json.loads(args)
+
+ args = frappe._dict(args)
+ doc = frappe.get_doc("Job Card", args.job_card_id)
+ doc.validate_sequence_id()
+ doc.add_time_log(args)
+
@frappe.whitelist()
def get_operation_details(work_order, operation):
if work_order and operation:
@@ -511,3 +666,28 @@ def get_job_details(start, end, filters=None):
events.append(job_card_data)
return events
+
+@frappe.whitelist()
+def make_corrective_job_card(source_name, operation=None, for_operation=None, target_doc=None):
+ def set_missing_values(source, target):
+ target.is_corrective_job_card = 1
+ target.operation = operation
+ target.for_operation = for_operation
+
+ target.set('time_logs', [])
+ target.set('employee', [])
+ target.set('items', [])
+ target.get_sub_operations()
+ target.get_required_items()
+ target.validate_time_logs()
+
+ doclist = get_mapped_doc("Job Card", source_name, {
+ "Job Card": {
+ "doctype": "Job Card",
+ "field_map": {
+ "name": "for_job_card",
+ },
+ }
+ }, target_doc, set_missing_values)
+
+ return doclist
\ No newline at end of file
diff --git a/erpnext/manufacturing/doctype/job_card_item/job_card_item.json b/erpnext/manufacturing/doctype/job_card_item/job_card_item.json
index 100ef4ca3a..d91530dd3b 100644
--- a/erpnext/manufacturing/doctype/job_card_item/job_card_item.json
+++ b/erpnext/manufacturing/doctype/job_card_item/job_card_item.json
@@ -25,8 +25,7 @@
"fieldtype": "Link",
"in_list_view": 1,
"label": "Item Code",
- "options": "Item",
- "read_only": 1
+ "options": "Item"
},
{
"fieldname": "source_warehouse",
@@ -67,8 +66,7 @@
"fieldname": "required_qty",
"fieldtype": "Float",
"in_list_view": 1,
- "label": "Required Qty",
- "read_only": 1
+ "label": "Required Qty"
},
{
"fieldname": "column_break_9",
@@ -107,7 +105,7 @@
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
- "modified": "2021-02-11 13:50:13.804108",
+ "modified": "2021-04-22 18:50:00.003444",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "Job Card Item",
diff --git a/erpnext/manufacturing/doctype/job_card_operation/__init__.py b/erpnext/manufacturing/doctype/job_card_operation/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/erpnext/manufacturing/doctype/job_card_operation/job_card_operation.json b/erpnext/manufacturing/doctype/job_card_operation/job_card_operation.json
new file mode 100644
index 0000000000..9a8692b84d
--- /dev/null
+++ b/erpnext/manufacturing/doctype/job_card_operation/job_card_operation.json
@@ -0,0 +1,59 @@
+{
+ "actions": [],
+ "creation": "2020-12-07 16:58:38.449041",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "sub_operation",
+ "completed_time",
+ "status",
+ "completed_qty"
+ ],
+ "fields": [
+ {
+ "default": "Pending",
+ "fieldname": "status",
+ "fieldtype": "Select",
+ "in_list_view": 1,
+ "label": "Status",
+ "options": "Complete\nPause\nPending\nWork In Progress",
+ "read_only": 1
+ },
+ {
+ "description": "In mins",
+ "fieldname": "completed_time",
+ "fieldtype": "Data",
+ "in_list_view": 1,
+ "label": "Completed Time",
+ "read_only": 1
+ },
+ {
+ "fieldname": "sub_operation",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "label": "Operation",
+ "options": "Operation",
+ "read_only": 1
+ },
+ {
+ "fieldname": "completed_qty",
+ "fieldtype": "Float",
+ "label": "Completed Qty",
+ "read_only": 1
+ }
+ ],
+ "index_web_pages_for_search": 1,
+ "istable": 1,
+ "links": [],
+ "modified": "2021-03-16 18:24:35.399593",
+ "modified_by": "Administrator",
+ "module": "Manufacturing",
+ "name": "Job Card Operation",
+ "owner": "Administrator",
+ "permissions": [],
+ "quick_entry": 1,
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "track_changes": 1
+}
\ No newline at end of file
diff --git a/erpnext/manufacturing/doctype/job_card_operation/job_card_operation.py b/erpnext/manufacturing/doctype/job_card_operation/job_card_operation.py
new file mode 100644
index 0000000000..85d72982ed
--- /dev/null
+++ b/erpnext/manufacturing/doctype/job_card_operation/job_card_operation.py
@@ -0,0 +1,10 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+from __future__ import unicode_literals
+# import frappe
+from frappe.model.document import Document
+
+class JobCardOperation(Document):
+ pass
diff --git a/erpnext/manufacturing/doctype/job_card_time_log/job_card_time_log.json b/erpnext/manufacturing/doctype/job_card_time_log/job_card_time_log.json
index 9dd54dd618..a7102d7d23 100644
--- a/erpnext/manufacturing/doctype/job_card_time_log/job_card_time_log.json
+++ b/erpnext/manufacturing/doctype/job_card_time_log/job_card_time_log.json
@@ -1,14 +1,17 @@
{
+ "actions": [],
"creation": "2019-03-08 23:56:43.187569",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
+ "employee",
"from_time",
"to_time",
"column_break_2",
"time_in_mins",
- "completed_qty"
+ "completed_qty",
+ "operation"
],
"fields": [
{
@@ -41,10 +44,27 @@
"in_list_view": 1,
"label": "Completed Qty",
"reqd": 1
+ },
+ {
+ "fieldname": "employee",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "label": "Employee",
+ "options": "Employee"
+ },
+ {
+ "fieldname": "operation",
+ "fieldtype": "Link",
+ "label": "Operation",
+ "no_copy": 1,
+ "options": "Operation",
+ "read_only": 1
}
],
+ "index_web_pages_for_search": 1,
"istable": 1,
- "modified": "2019-12-03 12:56:02.285448",
+ "links": [],
+ "modified": "2020-12-23 14:30:00.970916",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "Job Card Time Log",
diff --git a/erpnext/manufacturing/doctype/manufacturing_settings/manufacturing_settings.json b/erpnext/manufacturing/doctype/manufacturing_settings/manufacturing_settings.json
index b7634da87c..024f784725 100644
--- a/erpnext/manufacturing/doctype/manufacturing_settings/manufacturing_settings.json
+++ b/erpnext/manufacturing/doctype/manufacturing_settings/manufacturing_settings.json
@@ -26,7 +26,10 @@
"column_break_16",
"overproduction_percentage_for_work_order",
"other_settings_section",
- "update_bom_costs_automatically"
+ "update_bom_costs_automatically",
+ "add_corrective_operation_cost_in_finished_good_valuation",
+ "column_break_23",
+ "make_serial_no_batch_from_work_order"
],
"fields": [
{
@@ -155,13 +158,30 @@
{
"fieldname": "column_break_5",
"fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "column_break_23",
+ "fieldtype": "Column Break"
+ },
+ {
+ "default": "0",
+ "description": "System will automatically create the serial numbers / batch for the Finished Good on submission of work order",
+ "fieldname": "make_serial_no_batch_from_work_order",
+ "fieldtype": "Check",
+ "label": "Make Serial No / Batch from Work Order"
+ },
+ {
+ "default": "0",
+ "fieldname": "add_corrective_operation_cost_in_finished_good_valuation",
+ "fieldtype": "Check",
+ "label": "Add Corrective Operation Cost in Finished Good Valuation"
}
],
"icon": "icon-wrench",
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
- "modified": "2020-10-13 10:55:43.996581",
+ "modified": "2021-03-16 15:54:38.967341",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "Manufacturing Settings",
@@ -178,4 +198,4 @@
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
-}
+}
\ No newline at end of file
diff --git a/erpnext/manufacturing/doctype/operation/operation.js b/erpnext/manufacturing/doctype/operation/operation.js
index 5c2aba6f09..102b6780e5 100644
--- a/erpnext/manufacturing/doctype/operation/operation.js
+++ b/erpnext/manufacturing/doctype/operation/operation.js
@@ -2,7 +2,13 @@
// For license information, please see license.txt
frappe.ui.form.on('Operation', {
- refresh: function(frm) {
-
+ setup: function(frm) {
+ frm.set_query('operation', 'sub_operations', function() {
+ return {
+ filters: {
+ 'name': ['not in', [frm.doc.name]]
+ }
+ };
+ });
}
-});
+});
\ No newline at end of file
diff --git a/erpnext/manufacturing/doctype/operation/operation.json b/erpnext/manufacturing/doctype/operation/operation.json
index c231fba2fa..10a97eda76 100644
--- a/erpnext/manufacturing/doctype/operation/operation.json
+++ b/erpnext/manufacturing/doctype/operation/operation.json
@@ -1,167 +1,132 @@
{
- "allow_copy": 0,
- "allow_import": 1,
- "allow_rename": 1,
- "autoname": "Prompt",
- "beta": 0,
- "creation": "2014-11-07 16:20:30.683186",
- "custom": 0,
- "docstatus": 0,
- "doctype": "DocType",
- "document_type": "Setup",
- "editable_grid": 0,
- "engine": "InnoDB",
+ "actions": [],
+ "allow_import": 1,
+ "allow_rename": 1,
+ "autoname": "Prompt",
+ "creation": "2014-11-07 16:20:30.683186",
+ "doctype": "DocType",
+ "document_type": "Setup",
+ "engine": "InnoDB",
+ "field_order": [
+ "workstation",
+ "data_2",
+ "is_corrective_operation",
+ "job_card_section",
+ "create_job_card_based_on_batch_size",
+ "column_break_6",
+ "batch_size",
+ "sub_operations_section",
+ "sub_operations",
+ "total_operation_time",
+ "section_break_4",
+ "description"
+ ],
"fields": [
{
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "workstation",
- "fieldtype": "Link",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_list_view": 1,
- "in_standard_filter": 1,
- "label": "Default Workstation",
- "length": 0,
- "no_copy": 0,
- "options": "Workstation",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "unique": 0
- },
+ "fieldname": "workstation",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "in_standard_filter": 1,
+ "label": "Default Workstation",
+ "options": "Workstation"
+ },
{
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "section_break_4",
- "fieldtype": "Section Break",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "unique": 0
- },
+ "collapsible": 1,
+ "fieldname": "section_break_4",
+ "fieldtype": "Section Break",
+ "label": "Operation Description"
+ },
{
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "description",
- "fieldtype": "Text",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "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,
- "unique": 0
+ "fieldname": "description",
+ "fieldtype": "Text",
+ "label": "Description"
+ },
+ {
+ "collapsible": 1,
+ "fieldname": "sub_operations_section",
+ "fieldtype": "Section Break",
+ "label": "Sub Operations"
+ },
+ {
+ "fieldname": "sub_operations",
+ "fieldtype": "Table",
+ "options": "Sub Operation"
+ },
+ {
+ "description": "Time in mins.",
+ "fieldname": "total_operation_time",
+ "fieldtype": "Float",
+ "label": "Total Operation Time",
+ "read_only": 1
+ },
+ {
+ "fieldname": "data_2",
+ "fieldtype": "Column Break"
+ },
+ {
+ "default": "1",
+ "depends_on": "create_job_card_based_on_batch_size",
+ "fieldname": "batch_size",
+ "fieldtype": "Int",
+ "label": "Batch Size",
+ "mandatory_depends_on": "create_job_card_based_on_batch_size"
+ },
+ {
+ "default": "0",
+ "fieldname": "create_job_card_based_on_batch_size",
+ "fieldtype": "Check",
+ "label": "Create Job Card based on Batch Size"
+ },
+ {
+ "collapsible": 1,
+ "fieldname": "job_card_section",
+ "fieldtype": "Section Break",
+ "label": "Job Card"
+ },
+ {
+ "fieldname": "column_break_6",
+ "fieldtype": "Column Break"
+ },
+ {
+ "default": "0",
+ "fieldname": "is_corrective_operation",
+ "fieldtype": "Check",
+ "label": "Is Corrective Operation"
}
- ],
- "hide_heading": 0,
- "hide_toolbar": 0,
- "icon": "fa fa-wrench",
- "idx": 0,
- "image_view": 0,
- "in_create": 0,
-
- "is_submittable": 0,
- "issingle": 0,
- "istable": 0,
- "max_attachments": 0,
- "modified": "2016-11-07 05:28:27.462413",
- "modified_by": "Administrator",
- "module": "Manufacturing",
- "name": "Operation",
- "name_case": "",
- "owner": "Administrator",
+ ],
+ "icon": "fa fa-wrench",
+ "index_web_pages_for_search": 1,
+ "links": [],
+ "modified": "2021-01-12 15:09:23.593338",
+ "modified_by": "Administrator",
+ "module": "Manufacturing",
+ "name": "Operation",
+ "owner": "Administrator",
"permissions": [
{
- "amend": 0,
- "apply_user_permissions": 0,
- "cancel": 0,
- "create": 1,
- "delete": 1,
- "email": 0,
- "export": 1,
- "if_owner": 0,
- "import": 1,
- "is_custom": 0,
- "permlevel": 0,
- "print": 0,
- "read": 1,
- "report": 0,
- "role": "Manufacturing User",
- "set_user_permissions": 0,
- "share": 1,
- "submit": 0,
+ "create": 1,
+ "delete": 1,
+ "export": 1,
+ "import": 1,
+ "read": 1,
+ "role": "Manufacturing User",
+ "share": 1,
"write": 1
- },
+ },
{
- "amend": 0,
- "apply_user_permissions": 0,
- "cancel": 0,
- "create": 1,
- "delete": 1,
- "email": 0,
- "export": 1,
- "if_owner": 0,
- "import": 1,
- "is_custom": 0,
- "permlevel": 0,
- "print": 0,
- "read": 1,
- "report": 1,
- "role": "Manufacturing Manager",
- "set_user_permissions": 0,
- "share": 1,
- "submit": 0,
+ "create": 1,
+ "delete": 1,
+ "export": 1,
+ "import": 1,
+ "read": 1,
+ "report": 1,
+ "role": "Manufacturing Manager",
+ "share": 1,
"write": 1
}
- ],
- "quick_entry": 1,
- "read_only": 0,
- "read_only_onload": 0,
- "sort_field": "modified",
- "sort_order": "DESC",
- "track_seen": 0
+ ],
+ "quick_entry": 1,
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "track_changes": 1
}
\ No newline at end of file
diff --git a/erpnext/manufacturing/doctype/operation/operation.py b/erpnext/manufacturing/doctype/operation/operation.py
index 69e83292ff..374f32019b 100644
--- a/erpnext/manufacturing/doctype/operation/operation.py
+++ b/erpnext/manufacturing/doctype/operation/operation.py
@@ -2,9 +2,34 @@
# For license information, please see license.txt
from __future__ import unicode_literals
+
+import frappe
+from frappe import _
from frappe.model.document import Document
class Operation(Document):
def validate(self):
if not self.description:
self.description = self.name
+
+ self.duplicate_sub_operation()
+ self.set_total_time()
+
+ def duplicate_sub_operation(self):
+ operation_list = []
+ for row in self.sub_operations:
+ if row.operation in operation_list:
+ frappe.throw(_("The operation {0} can not add multiple times")
+ .format(frappe.bold(row.operation)))
+
+ if self.name == row.operation:
+ frappe.throw(_("The operation {0} can not be the sub operation")
+ .format(frappe.bold(row.operation)))
+
+ operation_list.append(row.operation)
+
+ def set_total_time(self):
+ self.total_operation_time = 0.0
+
+ for row in self.sub_operations:
+ self.total_operation_time += row.time_in_mins
diff --git a/erpnext/manufacturing/doctype/sub_operation/__init__.py b/erpnext/manufacturing/doctype/sub_operation/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/erpnext/manufacturing/doctype/sub_operation/sub_operation.js b/erpnext/manufacturing/doctype/sub_operation/sub_operation.js
new file mode 100644
index 0000000000..be9db6a408
--- /dev/null
+++ b/erpnext/manufacturing/doctype/sub_operation/sub_operation.js
@@ -0,0 +1,8 @@
+// Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
+// For license information, please see license.txt
+
+frappe.ui.form.on('Sub Operation', {
+ // refresh: function(frm) {
+
+ // }
+});
diff --git a/erpnext/manufacturing/doctype/sub_operation/sub_operation.json b/erpnext/manufacturing/doctype/sub_operation/sub_operation.json
new file mode 100644
index 0000000000..f63d2b9864
--- /dev/null
+++ b/erpnext/manufacturing/doctype/sub_operation/sub_operation.json
@@ -0,0 +1,51 @@
+{
+ "actions": [],
+ "creation": "2020-12-07 15:39:47.488519",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "operation",
+ "time_in_mins",
+ "column_break_5",
+ "description"
+ ],
+ "fields": [
+ {
+ "fieldname": "operation",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "label": "Operation",
+ "options": "Operation"
+ },
+ {
+ "description": "Time in mins",
+ "fieldname": "time_in_mins",
+ "fieldtype": "Float",
+ "in_list_view": 1,
+ "label": "Operation Time"
+ },
+ {
+ "fieldname": "column_break_5",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "description",
+ "fieldtype": "Small Text",
+ "label": "Description"
+ }
+ ],
+ "index_web_pages_for_search": 1,
+ "istable": 1,
+ "links": [],
+ "modified": "2020-12-07 18:09:18.005578",
+ "modified_by": "Administrator",
+ "module": "Manufacturing",
+ "name": "Sub Operation",
+ "owner": "Administrator",
+ "permissions": [],
+ "quick_entry": 1,
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "track_changes": 1
+}
\ No newline at end of file
diff --git a/erpnext/manufacturing/doctype/sub_operation/sub_operation.py b/erpnext/manufacturing/doctype/sub_operation/sub_operation.py
new file mode 100644
index 0000000000..f4b27758e9
--- /dev/null
+++ b/erpnext/manufacturing/doctype/sub_operation/sub_operation.py
@@ -0,0 +1,10 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+from __future__ import unicode_literals
+# import frappe
+from frappe.model.document import Document
+
+class SubOperation(Document):
+ pass
diff --git a/erpnext/manufacturing/doctype/sub_operation/test_sub_operation.py b/erpnext/manufacturing/doctype/sub_operation/test_sub_operation.py
new file mode 100644
index 0000000000..d3410ca312
--- /dev/null
+++ b/erpnext/manufacturing/doctype/sub_operation/test_sub_operation.py
@@ -0,0 +1,10 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors
+# See license.txt
+from __future__ import unicode_literals
+
+# import frappe
+import unittest
+
+class TestSubOperation(unittest.TestCase):
+ pass
diff --git a/erpnext/manufacturing/doctype/work_order/test_work_order.py b/erpnext/manufacturing/doctype/work_order/test_work_order.py
index cb1ee92196..68de0b29d3 100644
--- a/erpnext/manufacturing/doctype/work_order/test_work_order.py
+++ b/erpnext/manufacturing/doctype/work_order/test_work_order.py
@@ -389,17 +389,12 @@ class TestWorkOrder(unittest.TestCase):
ste.submit()
stock_entries.append(ste)
- job_cards = frappe.get_all('Job Card', filters = {'work_order': work_order.name})
+ job_cards = frappe.get_all('Job Card', filters = {'work_order': work_order.name}, order_by='creation asc')
self.assertEqual(len(job_cards), len(bom.operations))
for i, job_card in enumerate(job_cards):
doc = frappe.get_doc("Job Card", job_card)
- doc.append("time_logs", {
- "from_time": add_to_date(None, i),
- "hours": 1,
- "to_time": add_to_date(None, i + 1),
- "completed_qty": doc.for_quantity
- })
+ doc.time_logs[0].completed_qty = 1
doc.submit()
ste1 = frappe.get_doc(make_stock_entry(work_order.name, "Manufacture", 1))
diff --git a/erpnext/manufacturing/doctype/work_order/work_order.js b/erpnext/manufacturing/doctype/work_order/work_order.js
index 8088d930df..512048512e 100644
--- a/erpnext/manufacturing/doctype/work_order/work_order.js
+++ b/erpnext/manufacturing/doctype/work_order/work_order.js
@@ -141,8 +141,7 @@ frappe.ui.form.on("Work Order", {
}
if (frm.doc.docstatus === 1
- && frm.doc.operations && frm.doc.operations.length
- && frm.doc.qty != frm.doc.material_transferred_for_manufacturing) {
+ && frm.doc.operations && frm.doc.operations.length) {
const not_completed = frm.doc.operations.filter(d => {
if(d.status != 'Completed') {
@@ -190,35 +189,41 @@ frappe.ui.form.on("Work Order", {
const dialog = frappe.prompt({fieldname: 'operations', fieldtype: 'Table', label: __('Operations'),
fields: [
{
- fieldtype:'Link',
- fieldname:'operation',
+ fieldtype: 'Link',
+ fieldname: 'operation',
label: __('Operation'),
- read_only:1,
- in_list_view:1
+ read_only: 1,
+ in_list_view: 1
},
{
- fieldtype:'Link',
- fieldname:'workstation',
+ fieldtype: 'Link',
+ fieldname: 'workstation',
label: __('Workstation'),
- read_only:1,
- in_list_view:1
+ read_only: 1,
+ in_list_view: 1
},
{
- fieldtype:'Data',
- fieldname:'name',
+ fieldtype: 'Data',
+ fieldname: 'name',
label: __('Operation Id')
},
{
- fieldtype:'Float',
- fieldname:'pending_qty',
+ fieldtype: 'Float',
+ fieldname: 'pending_qty',
label: __('Pending Qty'),
},
{
- fieldtype:'Float',
- fieldname:'qty',
+ fieldtype: 'Float',
+ fieldname: 'qty',
label: __('Quantity to Manufacture'),
- read_only:0,
- in_list_view:1,
+ read_only: 0,
+ in_list_view: 1,
+ },
+ {
+ fieldtype: 'Float',
+ fieldname: 'batch_size',
+ label: __('Batch Size'),
+ read_only: 1
},
],
data: operations_data,
@@ -229,9 +234,13 @@ frappe.ui.form.on("Work Order", {
}, function(data) {
frappe.call({
method: "erpnext.manufacturing.doctype.work_order.work_order.make_job_card",
+ freeze: true,
args: {
work_order: frm.doc.name,
operations: data.operations,
+ },
+ callback: function() {
+ frm.reload_doc();
}
});
}, __("Job Card"), __("Create"));
@@ -243,13 +252,16 @@ frappe.ui.form.on("Work Order", {
if(data.completed_qty != frm.doc.qty) {
pending_qty = frm.doc.qty - flt(data.completed_qty);
- dialog.fields_dict.operations.df.data.push({
- 'name': data.name,
- 'operation': data.operation,
- 'workstation': data.workstation,
- 'qty': pending_qty,
- 'pending_qty': pending_qty,
- });
+ if (pending_qty) {
+ dialog.fields_dict.operations.df.data.push({
+ 'name': data.name,
+ 'operation': data.operation,
+ 'workstation': data.workstation,
+ 'batch_size': data.batch_size,
+ 'qty': pending_qty,
+ 'pending_qty': pending_qty
+ });
+ }
}
});
dialog.fields_dict.operations.grid.refresh();
diff --git a/erpnext/manufacturing/doctype/work_order/work_order.json b/erpnext/manufacturing/doctype/work_order/work_order.json
index cd9edeeea8..44d76d2b01 100644
--- a/erpnext/manufacturing/doctype/work_order/work_order.json
+++ b/erpnext/manufacturing/doctype/work_order/work_order.json
@@ -21,6 +21,12 @@
"produced_qty",
"sales_order",
"project",
+ "serial_no_and_batch_for_finished_good_section",
+ "has_serial_no",
+ "has_batch_no",
+ "column_break_17",
+ "serial_no",
+ "batch_size",
"settings_section",
"allow_alternative_item",
"use_multi_level_bom",
@@ -52,6 +58,7 @@
"actual_operating_cost",
"additional_operating_cost",
"column_break_24",
+ "corrective_operation_cost",
"total_operating_cost",
"more_info",
"description",
@@ -488,6 +495,57 @@
"fieldtype": "Float",
"label": "Lead Time",
"read_only": 1
+ },
+ {
+ "collapsible": 1,
+ "depends_on": "eval:!doc.__islocal",
+ "fieldname": "serial_no_and_batch_for_finished_good_section",
+ "fieldtype": "Section Break",
+ "label": "Serial No and Batch for Finished Good"
+ },
+ {
+ "fieldname": "column_break_17",
+ "fieldtype": "Column Break"
+ },
+ {
+ "default": "0",
+ "fetch_from": "production_item.has_serial_no",
+ "fieldname": "has_serial_no",
+ "fieldtype": "Check",
+ "label": "Has Serial No",
+ "read_only": 1
+ },
+ {
+ "default": "0",
+ "fetch_from": "production_item.has_batch_no",
+ "fieldname": "has_batch_no",
+ "fieldtype": "Check",
+ "label": "Has Batch No",
+ "read_only": 1
+ },
+ {
+ "depends_on": "has_serial_no",
+ "fieldname": "serial_no",
+ "fieldtype": "Small Text",
+ "label": "Serial Nos",
+ "no_copy": 1
+ },
+ {
+ "default": "0",
+ "depends_on": "has_batch_no",
+ "fieldname": "batch_size",
+ "fieldtype": "Float",
+ "label": "Batch Size"
+ },
+ {
+ "allow_on_submit": 1,
+ "description": "From Corrective Job Card",
+ "fieldname": "corrective_operation_cost",
+ "fieldtype": "Currency",
+ "label": "Corrective Operation Cost",
+ "no_copy": 1,
+ "print_hide": 1,
+ "read_only": 1
}
],
"icon": "fa fa-cogs",
@@ -495,7 +553,7 @@
"image_field": "image",
"is_submittable": 1,
"links": [],
- "modified": "2021-03-16 13:27:51.116484",
+ "modified": "2021-06-20 15:19:14.902699",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "Work Order",
diff --git a/erpnext/manufacturing/doctype/work_order/work_order.py b/erpnext/manufacturing/doctype/work_order/work_order.py
index c06cf81ad9..180815d80e 100644
--- a/erpnext/manufacturing/doctype/work_order/work_order.py
+++ b/erpnext/manufacturing/doctype/work_order/work_order.py
@@ -18,14 +18,16 @@ from frappe.utils.csvutils import getlink
from erpnext.stock.utils import get_bin, validate_warehouse_company, get_latest_stock_qty
from erpnext.utilities.transaction_base import validate_uom_is_integer
from frappe.model.mapper import get_mapped_doc
+from erpnext.stock.doctype.batch.batch import make_batch
+from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos, get_auto_serial_nos, auto_make_serial_nos
class OverProductionError(frappe.ValidationError): pass
class CapacityError(frappe.ValidationError): pass
class StockOverProductionError(frappe.ValidationError): pass
class OperationTooLongError(frappe.ValidationError): pass
class ItemHasVariantError(frappe.ValidationError): pass
-
-from six import string_types
+class SerialNoQtyError(frappe.ValidationError):
+ pass
class WorkOrder(Document):
@@ -123,7 +125,9 @@ class WorkOrder(Document):
variable_cost = self.actual_operating_cost if self.actual_operating_cost \
else self.planned_operating_cost
- self.total_operating_cost = flt(self.additional_operating_cost) + flt(variable_cost)
+
+ self.total_operating_cost = (flt(self.additional_operating_cost)
+ + flt(variable_cost) + flt(self.corrective_operation_cost))
def validate_work_order_against_so(self):
# already ordered qty
@@ -231,12 +235,15 @@ class WorkOrder(Document):
production_plan.run_method("update_produced_qty", produced_qty, self.production_plan_item)
+ def before_submit(self):
+ self.create_serial_no_batch_no()
+
def on_submit(self):
if not self.wip_warehouse:
frappe.throw(_("Work-in-Progress Warehouse is required before Submit"))
if not self.fg_warehouse:
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:
@@ -256,12 +263,76 @@ class WorkOrder(Document):
self.update_work_order_qty_in_combined_so()
else:
self.update_work_order_qty_in_so()
-
+
self.delete_job_card()
self.update_completed_qty_in_material_request()
self.update_planned_qty()
self.update_ordered_qty()
self.update_reserved_qty_for_production()
+ self.delete_auto_created_batch_and_serial_no()
+
+ def create_serial_no_batch_no(self):
+ if not (self.has_serial_no or self.has_batch_no):
+ return
+
+ if not cint(frappe.db.get_single_value("Manufacturing Settings", "make_serial_no_batch_from_work_order")):
+ return
+
+ if self.has_batch_no:
+ self.create_batch_for_finished_good()
+
+ args = {
+ "item_code": self.production_item,
+ "work_order": self.name
+ }
+
+ if self.has_serial_no:
+ self.make_serial_nos(args)
+
+ def create_batch_for_finished_good(self):
+ total_qty = self.qty
+ if not self.batch_size:
+ self.batch_size = total_qty
+
+ while total_qty > 0:
+ qty = self.batch_size
+ if self.batch_size >= total_qty:
+ qty = total_qty
+
+ if total_qty > self.batch_size:
+ total_qty -= self.batch_size
+ else:
+ qty = total_qty
+ total_qty = 0
+
+ make_batch(frappe._dict({
+ "item": self.production_item,
+ "qty_to_produce": qty,
+ "reference_doctype": self.doctype,
+ "reference_name": self.name
+ }))
+
+ def delete_auto_created_batch_and_serial_no(self):
+ for row in frappe.get_all("Serial No", filters = {"work_order": self.name}):
+ frappe.delete_doc("Serial No", row.name)
+ self.db_set("serial_no", "")
+
+ for row in frappe.get_all("Batch", filters = {"reference_name": self.name}):
+ frappe.delete_doc("Batch", row.name)
+
+ def make_serial_nos(self, args):
+ serial_no_series = frappe.get_cached_value("Item", self.production_item, "serial_no_series")
+ if serial_no_series:
+ self.serial_no = get_auto_serial_nos(serial_no_series, self.qty)
+
+ if self.serial_no:
+ args.update({"serial_no": self.serial_no, "actual_qty": self.qty})
+ auto_make_serial_nos(args)
+
+ serial_nos_length = len(get_serial_nos(self.serial_no))
+ if serial_nos_length != self.qty:
+ frappe.throw(_("{0} Serial Numbers required for Item {1}. You have provided {2}.")
+ .format(self.qty, self.production_item, serial_nos_length), SerialNoQtyError)
def create_job_card(self):
manufacturing_settings_doc = frappe.get_doc("Manufacturing Settings")
@@ -269,32 +340,40 @@ class WorkOrder(Document):
enable_capacity_planning = not cint(manufacturing_settings_doc.disable_capacity_planning)
plan_days = cint(manufacturing_settings_doc.capacity_planning_for_days) or 30
- for i, row in enumerate(self.operations):
- self.set_operation_start_end_time(i, row)
-
- if not row.workstation:
- frappe.throw(_("Row {0}: select the workstation against the operation {1}")
- .format(row.idx, row.operation))
-
- original_start_time = row.planned_start_time
- job_card_doc = create_job_card(self, row,
- enable_capacity_planning=enable_capacity_planning, auto_create=True)
-
- if enable_capacity_planning and job_card_doc:
- row.planned_start_time = job_card_doc.time_logs[-1].from_time
- row.planned_end_time = job_card_doc.time_logs[-1].to_time
-
- if date_diff(row.planned_start_time, original_start_time) > plan_days:
- frappe.message_log.pop()
- frappe.throw(_("Unable to find the time slot in the next {0} days for the operation {1}.")
- .format(plan_days, row.operation), CapacityError)
-
- row.db_update()
+ for index, row in enumerate(self.operations):
+ qty = self.qty
+ while qty > 0:
+ qty = split_qty_based_on_batch_size(self, row, qty)
+ if row.job_card_qty > 0:
+ self.prepare_data_for_job_card(row, index,
+ plan_days, enable_capacity_planning)
planned_end_date = self.operations and self.operations[-1].planned_end_time
if planned_end_date:
self.db_set("planned_end_date", planned_end_date)
+ def prepare_data_for_job_card(self, row, index, plan_days, enable_capacity_planning):
+ self.set_operation_start_end_time(index, row)
+
+ if not row.workstation:
+ frappe.throw(_("Row {0}: select the workstation against the operation {1}")
+ .format(row.idx, row.operation))
+
+ original_start_time = row.planned_start_time
+ job_card_doc = create_job_card(self, row, auto_create=True,
+ enable_capacity_planning=enable_capacity_planning)
+
+ if enable_capacity_planning and job_card_doc:
+ row.planned_start_time = job_card_doc.time_logs[-1].from_time
+ row.planned_end_time = job_card_doc.time_logs[-1].to_time
+
+ if date_diff(row.planned_start_time, original_start_time) > plan_days:
+ frappe.message_log.pop()
+ frappe.throw(_("Unable to find the time slot in the next {0} days for the operation {1}.")
+ .format(plan_days, row.operation), CapacityError)
+
+ row.db_update()
+
def set_operation_start_end_time(self, idx, row):
"""Set start and end time for given operation. If first operation, set start as
`planned_start_date`, else add time diff to end time of earlier operation."""
@@ -361,7 +440,7 @@ class WorkOrder(Document):
work_order_qty = qty[0][0] if qty and qty[0][0] else 0
frappe.db.set_value('Sales Order Item',
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:
@@ -374,7 +453,7 @@ class WorkOrder(Document):
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:
@@ -382,7 +461,7 @@ class WorkOrder(Document):
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):
if self.material_request:
frappe.get_doc("Material Request", self.material_request).update_completed_qty([self.material_request_item])
@@ -666,6 +745,17 @@ class WorkOrder(Document):
bom.set_bom_material_details()
return bom
+ def update_batch_produced_qty(self, stock_entry_doc):
+ if not cint(frappe.db.get_single_value("Manufacturing Settings", "make_serial_no_batch_from_work_order")):
+ return
+
+ for row in stock_entry_doc.items:
+ if row.batch_no and (row.is_finished_item or row.is_scrap_item):
+ qty = frappe.get_all("Stock Entry Detail", filters = {"batch_no": row.batch_no, "docstatus": 1},
+ or_filters= {"is_finished_item": 1, "is_scrap_item": 1}, fields = ["sum(qty)"], as_list=1)[0][0]
+
+ frappe.db.set_value("Batch", row.batch_no, "produced_qty", flt(qty))
+
@frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs
def get_bom_operations(doctype, txt, searchfield, start, page_len, filters):
@@ -743,7 +833,7 @@ def make_work_order(bom_no, item, qty=0, project=None, variant_items=None):
return wo_doc
def add_variant_item(variant_items, wo_doc, bom_no, table_name="items"):
- if isinstance(variant_items, string_types):
+ if isinstance(variant_items, str):
variant_items = json.loads(variant_items)
for item in variant_items:
@@ -823,6 +913,7 @@ def make_stock_entry(work_order_id, purpose, qty=None):
stock_entry.set_stock_entry_type()
stock_entry.get_items()
+ stock_entry.set_serial_no_batch_for_finished_good()
return stock_entry.as_dict()
@frappe.whitelist()
@@ -864,13 +955,47 @@ def query_sales_order(production_item):
@frappe.whitelist()
def make_job_card(work_order, operations):
- if isinstance(operations, string_types):
+ if isinstance(operations, str):
operations = json.loads(operations)
work_order = frappe.get_doc('Work Order', work_order)
for row in operations:
+ row = frappe._dict(row)
validate_operation_data(row)
- create_job_card(work_order, row, row.get("qty"), auto_create=True)
+ qty = row.get("qty")
+ while qty > 0:
+ qty = split_qty_based_on_batch_size(work_order, row, qty)
+ if row.job_card_qty > 0:
+ create_job_card(work_order, row, auto_create=True)
+
+def split_qty_based_on_batch_size(wo_doc, row, qty):
+ if not cint(frappe.db.get_value("Operation",
+ row.operation, "create_job_card_based_on_batch_size")):
+ row.batch_size = row.get("qty") or wo_doc.qty
+
+ row.job_card_qty = row.batch_size
+ if row.batch_size and qty >= row.batch_size:
+ qty -= row.batch_size
+ elif qty > 0:
+ row.job_card_qty = qty
+ qty = 0
+
+ get_serial_nos_for_job_card(row, wo_doc)
+
+ return qty
+
+def get_serial_nos_for_job_card(row, wo_doc):
+ if not wo_doc.serial_no:
+ return
+
+ serial_nos = get_serial_nos(wo_doc.serial_no)
+ used_serial_nos = []
+ for d in frappe.get_all('Job Card', fields=['serial_no'],
+ filters={'docstatus': ('<', 2), 'work_order': wo_doc.name, 'operation_id': row.name}):
+ used_serial_nos.extend(get_serial_nos(d.serial_no))
+
+ serial_nos = sorted(list(set(serial_nos) - set(used_serial_nos)))
+ row.serial_no = '\n'.join(serial_nos[0:row.job_card_qty])
def validate_operation_data(row):
if row.get("qty") <= 0:
@@ -889,20 +1014,22 @@ def validate_operation_data(row):
)
)
-def create_job_card(work_order, row, qty=0, enable_capacity_planning=False, auto_create=False):
+def create_job_card(work_order, row, enable_capacity_planning=False, auto_create=False):
doc = frappe.new_doc("Job Card")
doc.update({
'work_order': work_order.name,
'operation': row.get("operation"),
'workstation': row.get("workstation"),
'posting_date': nowdate(),
- 'for_quantity': qty or work_order.get('qty', 0),
+ 'for_quantity': row.job_card_qty or work_order.get('qty', 0),
'operation_id': row.get("name"),
'bom_no': work_order.bom_no,
'project': work_order.project,
'company': work_order.company,
'sequence_id': row.get("sequence_id"),
- 'wip_warehouse': work_order.wip_warehouse
+ 'wip_warehouse': work_order.wip_warehouse,
+ 'hour_rate': row.get("hour_rate"),
+ 'serial_no': row.get("serial_no")
})
if work_order.transfer_material_against == 'Job Card' and not work_order.skip_transfer:
diff --git a/erpnext/manufacturing/doctype/work_order/work_order_dashboard.py b/erpnext/manufacturing/doctype/work_order/work_order_dashboard.py
index 87c090f99c..9aa0715e7f 100644
--- a/erpnext/manufacturing/doctype/work_order/work_order_dashboard.py
+++ b/erpnext/manufacturing/doctype/work_order/work_order_dashboard.py
@@ -4,10 +4,17 @@ from frappe import _
def get_data():
return {
'fieldname': 'work_order',
+ 'non_standard_fieldnames': {
+ 'Batch': 'reference_name'
+ },
'transactions': [
{
'label': _('Transactions'),
'items': ['Stock Entry', 'Job Card', 'Pick List']
+ },
+ {
+ 'label': _('Reference'),
+ 'items': ['Serial No', 'Batch']
}
]
}
\ No newline at end of file
diff --git a/erpnext/manufacturing/doctype/work_order_operation/work_order_operation.json b/erpnext/manufacturing/doctype/work_order_operation/work_order_operation.json
index e28a42d39f..f7b8787a0b 100644
--- a/erpnext/manufacturing/doctype/work_order_operation/work_order_operation.json
+++ b/erpnext/manufacturing/doctype/work_order_operation/work_order_operation.json
@@ -7,8 +7,9 @@
"details",
"operation",
"bom",
- "sequence_id",
+ "column_break_4",
"description",
+ "sequence_id",
"col_break1",
"completed_qty",
"status",
@@ -198,6 +199,10 @@
"label": "Sequence ID",
"print_hide": 1,
"read_only": 1
+ },
+ {
+ "fieldname": "column_break_4",
+ "fieldtype": "Column Break"
}
],
"index_web_pages_for_search": 1,
diff --git a/erpnext/manufacturing/report/cost_of_poor_quality_report/__init__.py b/erpnext/manufacturing/report/cost_of_poor_quality_report/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/erpnext/manufacturing/report/cost_of_poor_quality_report/cost_of_poor_quality_report.js b/erpnext/manufacturing/report/cost_of_poor_quality_report/cost_of_poor_quality_report.js
new file mode 100644
index 0000000000..97e7e0a7d2
--- /dev/null
+++ b/erpnext/manufacturing/report/cost_of_poor_quality_report/cost_of_poor_quality_report.js
@@ -0,0 +1,105 @@
+// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors
+// For license information, please see license.txt
+/* eslint-disable */
+
+frappe.query_reports["Cost of Poor Quality Report"] = {
+ "filters": [
+ {
+ label: __("Company"),
+ fieldname: "company",
+ fieldtype: "Link",
+ options: "Company",
+ default: frappe.defaults.get_user_default("Company"),
+ reqd: 1
+ },
+ {
+ label: __("From Date"),
+ fieldname:"from_date",
+ fieldtype: "Datetime",
+ default: frappe.datetime.convert_to_system_tz(frappe.datetime.add_months(frappe.datetime.now_datetime(), -1)),
+ reqd: 1
+ },
+ {
+ label: __("To Date"),
+ fieldname:"to_date",
+ fieldtype: "Datetime",
+ default: frappe.datetime.now_datetime(),
+ reqd: 1,
+ },
+ {
+ label: __("Job Card"),
+ fieldname: "name",
+ fieldtype: "Link",
+ options: "Job Card",
+ get_query: function() {
+ return {
+ filters: {
+ is_corrective_job_card: 1,
+ docstatus: 1
+ }
+ }
+ }
+ },
+ {
+ label: __("Work Order"),
+ fieldname: "work_order",
+ fieldtype: "Link",
+ options: "Work Order"
+ },
+ {
+ label: __("Operation"),
+ fieldname: "operation",
+ fieldtype: "Link",
+ options: "Operation",
+ get_query: function() {
+ return {
+ filters: {
+ is_corrective_operation: 1
+ }
+ }
+ }
+ },
+ {
+ label: __("Workstation"),
+ fieldname: "workstation",
+ fieldtype: "Link",
+ options: "Workstation"
+ },
+ {
+ label: __("Item"),
+ fieldname: "production_item",
+ fieldtype: "Link",
+ options: "Item"
+ },
+ {
+ label: __("Serial No"),
+ fieldname: "serial_no",
+ fieldtype: "Link",
+ options: "Serial No",
+ depends_on: "eval: doc.production_item",
+ get_query: function() {
+ var item_code = frappe.query_report.get_filter_value('production_item');
+ return {
+ filters: {
+ item_code: item_code
+ }
+ }
+ }
+ },
+ {
+ label: __("Batch No"),
+ fieldname: "batch_no",
+ fieldtype: "Link",
+ options: "Batch No",
+ depends_on: "eval: doc.production_item",
+ get_query: function() {
+ var item_code = frappe.query_report.get_filter_value('production_item');
+ return {
+ filters: {
+ item: item_code
+ }
+ }
+ }
+ },
+ ]
+};
diff --git a/erpnext/manufacturing/report/cost_of_poor_quality_report/cost_of_poor_quality_report.json b/erpnext/manufacturing/report/cost_of_poor_quality_report/cost_of_poor_quality_report.json
new file mode 100644
index 0000000000..ee63bc1c28
--- /dev/null
+++ b/erpnext/manufacturing/report/cost_of_poor_quality_report/cost_of_poor_quality_report.json
@@ -0,0 +1,33 @@
+{
+ "add_total_row": 0,
+ "columns": [],
+ "creation": "2021-01-11 11:10:58.292896",
+ "disable_prepared_report": 0,
+ "disabled": 0,
+ "docstatus": 0,
+ "doctype": "Report",
+ "filters": [],
+ "idx": 0,
+ "is_standard": "Yes",
+ "json": "{}",
+ "modified": "2021-01-11 11:11:03.594242",
+ "modified_by": "Administrator",
+ "module": "Manufacturing",
+ "name": "Cost of Poor Quality Report",
+ "owner": "Administrator",
+ "prepared_report": 0,
+ "ref_doctype": "Job Card",
+ "report_name": "Cost of Poor Quality Report",
+ "report_type": "Script Report",
+ "roles": [
+ {
+ "role": "System Manager"
+ },
+ {
+ "role": "Manufacturing User"
+ },
+ {
+ "role": "Manufacturing Manager"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/erpnext/manufacturing/report/cost_of_poor_quality_report/cost_of_poor_quality_report.py b/erpnext/manufacturing/report/cost_of_poor_quality_report/cost_of_poor_quality_report.py
new file mode 100644
index 0000000000..9f81e7d26a
--- /dev/null
+++ b/erpnext/manufacturing/report/cost_of_poor_quality_report/cost_of_poor_quality_report.py
@@ -0,0 +1,127 @@
+# Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+from __future__ import unicode_literals
+import frappe
+from frappe import _
+from frappe.utils import flt
+
+def execute(filters=None):
+ columns, data = [], []
+
+ columns = get_columns(filters)
+ data = get_data(filters)
+
+ return columns, data
+
+def get_data(report_filters):
+ data = []
+ operations = frappe.get_all("Operation", filters = {"is_corrective_operation": 1})
+ if operations:
+ operations = [d.name for d in operations]
+ fields = ["production_item as item_code", "item_name", "work_order", "operation",
+ "workstation", "total_time_in_mins", "name", "hour_rate", "serial_no", "batch_no"]
+
+ filters = get_filters(report_filters, operations)
+
+ job_cards = frappe.get_all("Job Card", fields = fields,
+ filters = filters)
+
+ for row in job_cards:
+ row.operating_cost = flt(row.hour_rate) * (flt(row.total_time_in_mins) / 60.0)
+ update_raw_material_cost(row, report_filters)
+ data.append(row)
+
+ return data
+
+def get_filters(report_filters, operations):
+ filters = {"docstatus": 1, "operation": ("in", operations), "is_corrective_job_card": 1}
+ for field in ["name", "work_order", "operation", "workstation", "company", "serial_no", "batch_no", "production_item"]:
+ if report_filters.get(field):
+ if field != 'serial_no':
+ filters[field] = report_filters.get(field)
+ else:
+ filters[field] = ('like', '% {} %'.format(report_filters.get(field)))
+
+ return filters
+
+def update_raw_material_cost(row, filters):
+ row.rm_cost = 0.0
+ for data in frappe.get_all("Job Card Item", fields = ["amount"],
+ filters={"parent": row.name, "docstatus": 1}):
+ row.rm_cost += data.amount
+
+def get_columns(filters):
+ return [
+ {
+ "label": _("Job Card"),
+ "fieldtype": "Link",
+ "fieldname": "name",
+ "options": "Job Card",
+ "width": "100"
+ },
+ {
+ "label": _("Work Order"),
+ "fieldtype": "Link",
+ "fieldname": "work_order",
+ "options": "Work Order",
+ "width": "100"
+ },
+ {
+ "label": _("Item Code"),
+ "fieldtype": "Link",
+ "fieldname": "item_code",
+ "options": "Item",
+ "width": "100"
+ },
+ {
+ "label": _("Item Name"),
+ "fieldtype": "Data",
+ "fieldname": "item_name",
+ "width": "100"
+ },
+ {
+ "label": _("Operation"),
+ "fieldtype": "Link",
+ "fieldname": "operation",
+ "options": "Operation",
+ "width": "100"
+ },
+ {
+ "label": _("Serial No"),
+ "fieldtype": "Data",
+ "fieldname": "serial_no",
+ "width": "100"
+ },
+ {
+ "label": _("Batch No"),
+ "fieldtype": "Data",
+ "fieldname": "batch_no",
+ "width": "100"
+ },
+ {
+ "label": _("Workstation"),
+ "fieldtype": "Link",
+ "fieldname": "workstation",
+ "options": "Workstation",
+ "width": "100"
+ },
+ {
+ "label": _("Operating Cost"),
+ "fieldtype": "Currency",
+ "fieldname": "operating_cost",
+ "width": "100"
+ },
+ {
+ "label": _("Raw Material Cost"),
+ "fieldtype": "Currency",
+ "fieldname": "rm_cost",
+ "width": "100"
+ },
+ {
+ "label": _("Total Time (in Mins)"),
+ "fieldtype": "Float",
+ "fieldname": "total_time_in_mins",
+ "width": "100"
+ }
+ ]
\ No newline at end of file
diff --git a/erpnext/patches.txt b/erpnext/patches.txt
index 339e7f99d2..986b0c5711 100644
--- a/erpnext/patches.txt
+++ b/erpnext/patches.txt
@@ -290,3 +290,5 @@ erpnext.patches.v13_0.add_doctype_to_sla #14-06-2021
erpnext.patches.v13_0.set_training_event_attendance
erpnext.patches.v13_0.bill_for_rejected_quantity_in_purchase_invoice
erpnext.patches.v13_0.rename_issue_status_hold_to_on_hold
+erpnext.patches.v13_0.bill_for_rejected_quantity_in_purchase_invoice
+erpnext.patches.v13_0.update_job_card_details
diff --git a/erpnext/patches/v13_0/update_job_card_details.py b/erpnext/patches/v13_0/update_job_card_details.py
new file mode 100644
index 0000000000..d4e65c6f2f
--- /dev/null
+++ b/erpnext/patches/v13_0/update_job_card_details.py
@@ -0,0 +1,16 @@
+# Copyright (c) 2019, Frappe and Contributors
+# License: GNU General Public License v3. See license.txt
+
+from __future__ import unicode_literals
+import frappe
+
+def execute():
+ frappe.reload_doc("manufacturing", "doctype", "job_card")
+ frappe.reload_doc("manufacturing", "doctype", "job_card_item")
+ frappe.reload_doc("manufacturing", "doctype", "work_order_operation")
+
+ frappe.db.sql(""" update `tabJob Card` jc, `tabWork Order Operation` wo
+ SET jc.hour_rate = wo.hour_rate
+ WHERE
+ jc.operation_id = wo.name and jc.docstatus < 2 and wo.hour_rate > 0
+ """)
\ No newline at end of file
diff --git a/erpnext/payroll/doctype/payroll_entry/payroll_entry.js b/erpnext/payroll/doctype/payroll_entry/payroll_entry.js
index f2892600d1..496c37b2fa 100644
--- a/erpnext/payroll/doctype/payroll_entry/payroll_entry.js
+++ b/erpnext/payroll/doctype/payroll_entry/payroll_entry.js
@@ -135,10 +135,26 @@ frappe.ui.form.on('Payroll Entry', {
});
frm.set_query('employee', 'employees', () => {
- if (!frm.doc.company) {
- frappe.msgprint(__("Please set a Company"));
- return [];
+ let error_fields = [];
+ let mandatory_fields = ['company', 'payroll_frequency', 'start_date', 'end_date'];
+
+ let message = __('Mandatory fields required in {0}', [__(frm.doc.doctype)]);
+
+ mandatory_fields.forEach(field => {
+ if (!frm.doc[field]) {
+ error_fields.push(frappe.unscrub(field));
+ }
+ });
+
+ if (error_fields && error_fields.length) {
+ message = message + '
- -
+-
{{ _("No tasks") }}
-{% endif %} + {{ progress_bar(doc.percent_complete) }} + {% if doc.tasks %} +{{ _("No Tasks") }}
+ {% endif %} - + {% if doc.timesheets %} +{{ _("No Timesheets") }}
+ {% endif %} -- {% endif %} -{% else %} -
{{ _("No time sheets") }}
-{% endif %} - -{% if doc.attachments %} - - -